From 1a376cfcde0bc89b59d9c5049a5e99bd973048a4 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 10 Oct 2024 10:32:03 -0700 Subject: [PATCH 01/69] Make context mockable Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 +++ .../DaprWorkflowActivityContext.cs | 37 ++++++++++++++ src/Dapr.Workflow/WorkflowActivityContext.cs | 14 ++--- src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 4 +- .../Dapr.Workflow.Test.csproj | 25 +++++++++ .../WorkflowActivityTest.cs | 51 +++++++++++++++++++ 6 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/Dapr.Workflow/DaprWorkflowActivityContext.cs create mode 100644 test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj create mode 100644 test/Dapr.Workflow.Test/WorkflowActivityTest.cs diff --git a/all.sln b/all.sln index 228047852..34248e70d 100644 --- a/all.sln +++ b/all.sln @@ -118,6 +118,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflows.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -290,6 +292,10 @@ Global {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 + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,6 +349,7 @@ Global {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} + {7CA93D67-C551-430E-AA2C-BC64B77F7908} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Workflow/DaprWorkflowActivityContext.cs b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs new file mode 100644 index 000000000..cf902dea4 --- /dev/null +++ b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// Copyright 2022 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow +{ + using System; + using Microsoft.DurableTask; + + /// + /// Defines properties and methods for task activity context objects. + /// + public class DaprWorkflowActivityContext : WorkflowActivityContext + { + readonly TaskActivityContext innerContext; + + internal DaprWorkflowActivityContext(TaskActivityContext innerContext) + { + this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); + } + + /// + public override TaskName Name => this.innerContext.Name; + + /// + public override string InstanceId => this.innerContext.InstanceId; + } +} diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index eec32f008..a77c3ef91 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -13,29 +13,21 @@ namespace Dapr.Workflow { - using System; using Microsoft.DurableTask; /// /// Defines properties and methods for task activity context objects. /// - public class WorkflowActivityContext + public abstract class WorkflowActivityContext { - readonly TaskActivityContext innerContext; - - internal WorkflowActivityContext(TaskActivityContext innerContext) - { - this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); - } - /// /// Gets the name of the activity. /// - public TaskName Name => this.innerContext.Name; + public abstract TaskName Name { get; } /// /// Gets the unique ID of the current workflow instance. /// - public string InstanceId => this.innerContext.InstanceId; + public abstract string InstanceId { get; } } } diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index adc925777..e2fbf913a 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -90,7 +90,7 @@ public void RegisterActivity(string name, Func(name, (innerContext, input) => { - WorkflowActivityContext activityContext = new(innerContext); + WorkflowActivityContext activityContext = new DaprWorkflowActivityContext(innerContext); return implementation(activityContext, input); }); WorkflowLoggingService.LogActivityName(name); @@ -167,7 +167,7 @@ public ActivityWrapper(IWorkflowActivity activity) public Task RunAsync(TaskActivityContext context, object? input) { - return this.activity.RunAsync(new WorkflowActivityContext(context), input); + return this.activity.RunAsync(new DaprWorkflowActivityContext(context), input); } } } diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj new file mode 100644 index 000000000..15e626772 --- /dev/null +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -0,0 +1,25 @@ + + + Dapr.Workflow.Tests + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs new file mode 100644 index 000000000..9fd96e60b --- /dev/null +++ b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// 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.Workflow.Test +{ + using Moq; + using System.Threading.Tasks; + using Xunit; + + /// + /// Contains tests for WorkflowActivityContext. + /// + public class WorkflowActivityTest + { + private IWorkflowActivity workflowActivity; + + private Mock workflowActivityContextMock; + + [Fact] + public async Task RunAsync_ShouldReturnCorrectContextInstanceId() + { + this.workflowActivity = new TestDaprWorkflowActivity(); + this.workflowActivityContextMock = new Mock(); + + this.workflowActivityContextMock.Setup((x) => x.InstanceId).Returns("instanceId"); + + string result = (string) await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"); + + Assert.Equal("instanceId", result); + } + + + public class TestDaprWorkflowActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, string input) + { + return Task.FromResult(context.InstanceId); + } + } + } +} From f51def0bd7e35349aeb84f1155eb86fba35f9e23 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 10 Oct 2024 10:38:13 -0700 Subject: [PATCH 02/69] Fix project Signed-off-by: Siri Varma Vegiraju --- all.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/all.sln b/all.sln index 34248e70d..0425b00f2 100644 --- a/all.sln +++ b/all.sln @@ -118,7 +118,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflows.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 43d8ee239f17280c24625399bd51daa722a740e0 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo Date: Wed, 26 Jun 2024 11:24:20 +0200 Subject: [PATCH 03/69] Consolidated version of coverlet.msbuild, coverlet.collector, xunit, xunit.runner.visualstudio, Microsoft.AspNetCore.Mvc.Testing, Moq to the same version in all projects. Signed-off-by: Manuel Menegazzo Signed-off-by: Siri Varma Vegiraju --- .../WorkflowUnitTest/WorkflowUnitTest.csproj | 8 ++++---- ...apr.Actors.AspNetCore.IntegrationTest.csproj | 8 ++++---- .../Dapr.Actors.AspNetCore.Test.csproj | 8 ++++---- .../Dapr.Actors.Generators.Test.csproj | 6 +++--- test/Dapr.Actors.Test/Dapr.Actors.Test.csproj | 8 ++++---- .../Dapr.AspNetCore.IntegrationTest.csproj | 8 ++++---- .../Dapr.AspNetCore.Test.csproj | 6 +++--- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 10 +++++----- .../Dapr.E2E.Test.Actors.Generators.csproj | 12 +++++------- test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 4 ++-- test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 17 ++++++++--------- .../Dapr.Extensions.Configuration.Test.csproj | 8 ++++---- 12 files changed, 50 insertions(+), 53 deletions(-) diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 4ce0c9801..7163f4e0c 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -8,13 +8,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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..c44d19f61 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 83ea494a8..9a8b55c2f 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.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.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 212faed2d..02aaf1bb3 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -18,12 +18,12 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8852dd465..d87ea3cd3 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.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index 3cd79d908..ed110191f 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,15 +1,15 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index aa463be98..32416dd8a 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 diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index aef5b4113..06322f4d1 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,7 +1,7 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -11,11 +11,11 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 index 8618647cb..6ef9c009d 100644 --- 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 @@ -9,14 +9,14 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -24,9 +24,7 @@ - + diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index f899167c4..be3027269 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -5,8 +5,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index d95929ca3..2079c7ac6 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -11,15 +11,14 @@ // limitations under the License. // ------------------------------------------------------------------------ using System; -using System.IO; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapr.Client; using FluentAssertions; using Xunit; -using System.Linq; -using System.Diagnostics; namespace Dapr.E2E.Test { @@ -43,7 +42,7 @@ public async Task TestWorkflowLogging() var health = await daprClient.CheckHealthAsync(); health.Should().Be(true, "DaprClient is not healthy"); - var searchTask = Task.Run(async() => + var searchTask = Task.Run(async () => { using (StreamReader reader = new StreamReader(logFilePath)) { @@ -76,7 +75,7 @@ public async Task TestWorkflowLogging() } if (!allLogsFound) { - Assert.True(false, "The logs were not able to found within the timeout"); + Assert.Fail("The logs were not able to found within the timeout"); } } [Fact] @@ -96,7 +95,7 @@ public async Task TestWorkflows() // START WORKFLOW TEST var startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId, + instanceId: instanceId, workflowComponent: workflowComponent, workflowName: workflowName, input: input, @@ -131,10 +130,10 @@ public async Task TestWorkflows() // PURGE TEST await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent); - try + try { getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - Assert.True(false, "The GetWorkflowAsync call should have failed since the instance was purged"); + Assert.Fail("The GetWorkflowAsync call should have failed since the instance was purged"); } catch (DaprException ex) { @@ -159,7 +158,7 @@ public async Task TestWorkflows() 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); 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 7d11d5c40..d259f2ab1 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,15 +1,15 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 3ce4685f63cd0fe75afa4f100e958de17b91f065 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 10 Oct 2024 10:32:03 -0700 Subject: [PATCH 04/69] Make context mockable Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 +++ .../DaprWorkflowActivityContext.cs | 37 ++++++++++++++ src/Dapr.Workflow/WorkflowActivityContext.cs | 14 ++--- src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 4 +- .../Dapr.Workflow.Test.csproj | 25 +++++++++ .../WorkflowActivityTest.cs | 51 +++++++++++++++++++ 6 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/Dapr.Workflow/DaprWorkflowActivityContext.cs create mode 100644 test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj create mode 100644 test/Dapr.Workflow.Test/WorkflowActivityTest.cs diff --git a/all.sln b/all.sln index 228047852..34248e70d 100644 --- a/all.sln +++ b/all.sln @@ -118,6 +118,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflows.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -290,6 +292,10 @@ Global {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 + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,6 +349,7 @@ Global {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} + {7CA93D67-C551-430E-AA2C-BC64B77F7908} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.Workflow/DaprWorkflowActivityContext.cs b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs new file mode 100644 index 000000000..cf902dea4 --- /dev/null +++ b/src/Dapr.Workflow/DaprWorkflowActivityContext.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// Copyright 2022 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow +{ + using System; + using Microsoft.DurableTask; + + /// + /// Defines properties and methods for task activity context objects. + /// + public class DaprWorkflowActivityContext : WorkflowActivityContext + { + readonly TaskActivityContext innerContext; + + internal DaprWorkflowActivityContext(TaskActivityContext innerContext) + { + this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); + } + + /// + public override TaskName Name => this.innerContext.Name; + + /// + public override string InstanceId => this.innerContext.InstanceId; + } +} diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index eec32f008..a77c3ef91 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -13,29 +13,21 @@ namespace Dapr.Workflow { - using System; using Microsoft.DurableTask; /// /// Defines properties and methods for task activity context objects. /// - public class WorkflowActivityContext + public abstract class WorkflowActivityContext { - readonly TaskActivityContext innerContext; - - internal WorkflowActivityContext(TaskActivityContext innerContext) - { - this.innerContext = innerContext ?? throw new ArgumentNullException(nameof(innerContext)); - } - /// /// Gets the name of the activity. /// - public TaskName Name => this.innerContext.Name; + public abstract TaskName Name { get; } /// /// Gets the unique ID of the current workflow instance. /// - public string InstanceId => this.innerContext.InstanceId; + public abstract string InstanceId { get; } } } diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index adc925777..e2fbf913a 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -90,7 +90,7 @@ public void RegisterActivity(string name, Func(name, (innerContext, input) => { - WorkflowActivityContext activityContext = new(innerContext); + WorkflowActivityContext activityContext = new DaprWorkflowActivityContext(innerContext); return implementation(activityContext, input); }); WorkflowLoggingService.LogActivityName(name); @@ -167,7 +167,7 @@ public ActivityWrapper(IWorkflowActivity activity) public Task RunAsync(TaskActivityContext context, object? input) { - return this.activity.RunAsync(new WorkflowActivityContext(context), input); + return this.activity.RunAsync(new DaprWorkflowActivityContext(context), input); } } } diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj new file mode 100644 index 000000000..15e626772 --- /dev/null +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -0,0 +1,25 @@ + + + Dapr.Workflow.Tests + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs new file mode 100644 index 000000000..9fd96e60b --- /dev/null +++ b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// 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.Workflow.Test +{ + using Moq; + using System.Threading.Tasks; + using Xunit; + + /// + /// Contains tests for WorkflowActivityContext. + /// + public class WorkflowActivityTest + { + private IWorkflowActivity workflowActivity; + + private Mock workflowActivityContextMock; + + [Fact] + public async Task RunAsync_ShouldReturnCorrectContextInstanceId() + { + this.workflowActivity = new TestDaprWorkflowActivity(); + this.workflowActivityContextMock = new Mock(); + + this.workflowActivityContextMock.Setup((x) => x.InstanceId).Returns("instanceId"); + + string result = (string) await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"); + + Assert.Equal("instanceId", result); + } + + + public class TestDaprWorkflowActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, string input) + { + return Task.FromResult(context.InstanceId); + } + } + } +} From d0834832a900972f04681c59167c7cbe15381c24 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Thu, 10 Oct 2024 10:38:13 -0700 Subject: [PATCH 05/69] Fix project Signed-off-by: Siri Varma Vegiraju --- all.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/all.sln b/all.sln index 34248e70d..0425b00f2 100644 --- a/all.sln +++ b/all.sln @@ -118,7 +118,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflows.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 06539029f86008730dcd98dbaf6e6ffc1140a622 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 13 Sep 2024 00:38:41 -0500 Subject: [PATCH 06/69] Added unit test to prove out concern raised on Discord Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- test/Dapr.Client.Test/SecretApiTest.cs | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/Dapr.Client.Test/SecretApiTest.cs b/test/Dapr.Client.Test/SecretApiTest.cs index c94c82844..3e21440e3 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. @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using FluentAssertions.Equivalency; + namespace Dapr.Client.Test { using System; @@ -93,6 +95,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() { From a7c37ce114718f65de601e153e693a3f0027ae47 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Sep 2024 08:28:04 -0500 Subject: [PATCH 07/69] Removed unused using Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- test/Dapr.Client.Test/SecretApiTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Dapr.Client.Test/SecretApiTest.cs b/test/Dapr.Client.Test/SecretApiTest.cs index 3e21440e3..26048e2a4 100644 --- a/test/Dapr.Client.Test/SecretApiTest.cs +++ b/test/Dapr.Client.Test/SecretApiTest.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using FluentAssertions.Equivalency; - namespace Dapr.Client.Test { using System; From 2b408c69379f9ba960ddc86d8e1df90c4cda5425 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 7 Oct 2024 10:46:39 -0500 Subject: [PATCH 08/69] Added unit test to validate that headers aren't being stripped off request Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../DaprClientTest.InvokeMethodAsync.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 484f327d0..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; @@ -654,8 +657,34 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQuery 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() From 8277fc23694c993c4dcbaad2029d2404807c2c00 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 7 Oct 2024 10:47:24 -0500 Subject: [PATCH 09/69] Fixed spelling typo Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- test/Shared/TestClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 Date: Sun, 6 Oct 2024 02:29:53 -0500 Subject: [PATCH 10/69] Added fix to handle null return values Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Client/DaprClientGrpc.cs | 15 +++++++++++---- src/Dapr.Client/DaprMetadata.cs | 1 - 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index af245afc3..84d0a1117 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -2390,10 +2390,17 @@ public override async Task GetMetadataAsync(CancellationToken canc try { var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - return new DaprMetadata(response.Id, - response.ActorRuntime.ActiveActors.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList(), - 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()); + if (response is null) + return null; + + 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/DaprMetadata.cs b/src/Dapr.Client/DaprMetadata.cs index a58707c99..4cd812e04 100644 --- a/src/Dapr.Client/DaprMetadata.cs +++ b/src/Dapr.Client/DaprMetadata.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Collections.Generic; namespace Dapr.Client From cf9f1ca7e997a6e8cffdc81c441b5914d9fc2a3b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 11 Oct 2024 01:47:37 -0500 Subject: [PATCH 11/69] Removed unnecessary null check Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Client/DaprClientGrpc.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 84d0a1117..a5f5833a2 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -2390,9 +2390,6 @@ public override async Task GetMetadataAsync(CancellationToken canc try { var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - if (response is null) - return null; - return new DaprMetadata(response.Id ?? "", response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? new List(), From 5dcc4e5ce2bad8c0fc39e792c1245a1b74342a53 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 3 Sep 2024 06:30:41 -0500 Subject: [PATCH 12/69] Removed deprecated methods from DaprClient and tests as well as unused types Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- examples/Actor/ActorClient/Program.cs | 6 +- examples/Actor/DemoActor/BankService.cs | 4 +- examples/Actor/DemoActor/DemoActor.cs | 16 +- examples/Actor/DemoActor/Program.cs | 8 +- examples/Actor/DemoActor/Startup.cs | 15 +- examples/Actor/IDemoActor/IBankActor.cs | 10 +- examples/Actor/IDemoActor/IDemoActor.cs | 11 +- .../ControllerSample/CustomTopicAttribute.cs | 2 + .../AspNetCore/ControllerSample/Startup.cs | 2 + .../Services/BankingService.cs | 2 +- .../AspNetCore/GrpcServiceSample/Startup.cs | 2 + examples/Client/ConfigurationApi/Startup.cs | 1 + examples/Client/Cryptography/Program.cs | 3 +- examples/Client/DistributedLock/Startup.cs | 3 +- .../Activities/ProcessPaymentActivity.cs | 1 - .../Activities/RequestApprovalActivity.cs | 1 - .../Activities/ReserveInventoryActivity.cs | 1 - .../Activities/UpdateInventoryActivity.cs | 1 - .../Workflow/WorkflowConsoleApp/Models.cs | 2 +- .../Workflow/WorkflowConsoleApp/Program.cs | 2 +- .../Workflows/OrderProcessingWorkflow.cs | 1 - .../WorkflowUnitTest/OrderProcessingTests.cs | 2 +- src/Dapr.Client/DaprClient.cs | 187 +----------- src/Dapr.Client/DaprClientGrpc.cs | 281 ------------------ src/Dapr.Client/GetWorkflowResponse.cs | 100 ------- src/Dapr.Client/WorkflowFailureDetails.cs | 35 --- .../WorkflowRuntimeStatus.cs | 2 +- test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 90 +----- 28 files changed, 53 insertions(+), 738 deletions(-) delete mode 100644 src/Dapr.Client/GetWorkflowResponse.cs delete mode 100644 src/Dapr.Client/WorkflowFailureDetails.cs rename src/{Dapr.Client => Dapr.Workflow}/WorkflowRuntimeStatus.cs (98%) diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index bae5d2ec2..f6ca26f53 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -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,7 +45,7 @@ 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, TimeSpan.FromMinutes(10)); 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 da780d517..b5ef53e93 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -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"; 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 da2b9e764..881bc6a27 100644 --- a/examples/Actor/DemoActor/Startup.cs +++ b/examples/Actor/DemoActor/Startup.cs @@ -11,14 +11,15 @@ // 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 Dapr.Actors.AspNetCore; +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/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 25ce09370..6f2d32801 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -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. /// 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..64cfba512 100644 --- a/examples/AspNetCore/ControllerSample/Startup.cs +++ b/examples/AspNetCore/ControllerSample/Startup.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.AspNetCore; + namespace ControllerSample { using Microsoft.AspNetCore.Builder; 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/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/Program.cs b/examples/Client/Cryptography/Program.cs index 74e3c7f48..da81bef8f 100644 --- a/examples/Client/Cryptography/Program.cs +++ b/examples/Client/Cryptography/Program.cs @@ -11,10 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Cryptography; using Cryptography.Examples; -namespace Samples.Client +namespace Cryptography { class Program { 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/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 2b8213887..26d34615d 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -1,10 +1,10 @@ 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"; 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/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 4f89d8668..94a43b759 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -1296,192 +1296,7 @@ public abstract Task Unlock( string resourceId, string lockOwner, CancellationToken cancellationToken = default); - - /// - /// Attempt to start the given workflow with response indicating success. - /// - /// 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, - CancellationToken cancellationToken = default); - - /// - /// Waits for a workflow to start running and returns a object that contains metadata - /// about the started workflow. - /// - /// - /// - /// A "started" workflow instance is any instance not in the state. - /// - /// This method will return a completed task if the workflow has already started running or has already completed. - /// - /// - /// The unique ID of the workflow instance to wait for. - /// 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); - } - } - - /// - /// Attempt to get information about the given workflow. - /// - /// 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, - CancellationToken cancellationToken = default); - - /// - /// Attempt to get terminate the given workflow. - /// - /// 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); - - /// - /// Attempt to raise an event the given workflow with response indicating success. - /// - /// 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); - - - /// - /// Pauses the specified workflow instance. - /// - /// 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); - - - /// - /// Resumes a paused workflow instance. - /// - /// 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 resume 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 ResumeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - - /// - /// Delete all state associated with the specified workflow instance. The workflow must be in a non-running state to be purged. - /// - /// 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 purge 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 PurgeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default); - + /// public void Dispose() { diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index a5f5833a2..c0c0015e0 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -2036,287 +2036,6 @@ public async override Task Unlock( #endregion - - #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) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowName, nameof(workflowName)); - ArgumentVerifier.ThrowIfNull(input, nameof(input)); - - // Serialize json data. Converts input object to bytes and then bytestring inside the request. - byte[] jsonUtf8Bytes = null; - if (input is not null) - { - jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); - } - - 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) - { - request.Options[item.Key] = item.Value; - } - } - - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.StartWorkflowAlpha1Async(request, options); - return new StartWorkflowResponse(response.InstanceId); - - } - catch (RpcException ex) - { - throw new DaprException("Start Workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - /// - [Obsolete] - public async override Task GetWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.GetWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - 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; - - 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); - } - } - - private static WorkflowRuntimeStatus GetWorkflowRuntimeStatus(string runtimeStatus) - { - if (!System.Enum.TryParse(runtimeStatus, true /* ignoreCase */, out WorkflowRuntimeStatus status)) - { - status = WorkflowRuntimeStatus.Unknown; - } - - return status; - } - - private static WorkflowFailureDetails GetWorkflowFailureDetails(Autogenerated.GetWorkflowResponse response, string componentName) - { - // 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)) - { - return new WorkflowFailureDetails(errorMessage, errorType, stackTrace); - } - - return null; - } - - /// - [Obsolete] - public async override Task TerminateWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.TerminateWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - 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); - } - - } - - /// - [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); - } - - var request = new Autogenerated.RaiseEventWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent, - EventName = eventName, - EventData = ByteString.CopyFrom(jsonUtf8Bytes), - }; - - 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); - } - } - - - /// - [Obsolete] - public async override Task PauseWorkflowAsync( - string instanceId, - string workflowComponent, - 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); - - try - { - await client.PauseWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Pause workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - - /// - [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() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.ResumeWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Resume workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - - } - - /// - [Obsolete] - public async override Task PurgeWorkflowAsync( - string instanceId, - string workflowComponent, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); - - var request = new Autogenerated.PurgeWorkflowRequest() - { - InstanceId = instanceId, - WorkflowComponent = workflowComponent - }; - - var options = CreateCallOptions(headers: null, cancellationToken); - - try - { - await client.PurgeWorkflowAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Purge workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - - } - #endregion - - #region Dapr Sidecar Methods /// 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/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/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/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index 2079c7ac6..5ed5f50ee 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -19,6 +19,7 @@ using Dapr.Client; using FluentAssertions; using Xunit; +using Dapr.Workflow; namespace Dapr.E2E.Test { @@ -78,94 +79,5 @@ public async Task TestWorkflowLogging() Assert.Fail("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.Fail("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"); - } } } From 11b3aec70b3d753580f66c2a056b3f639e5e62eb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 3 Sep 2024 18:45:36 -0500 Subject: [PATCH 13/69] Removed unused (and invalid) reference Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- examples/Actor/DemoActor/Startup.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs index 881bc6a27..f1165e3c7 100644 --- a/examples/Actor/DemoActor/Startup.cs +++ b/examples/Actor/DemoActor/Startup.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.Actors.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; From c6eb1d838c65dbe2bb7f2fd3d45d40edef6aa800 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 11 Oct 2024 02:13:06 -0500 Subject: [PATCH 14/69] Removed E2E workflow test as it validated DaprClient and the functionality has been moved out to the Dapr.Workflow project instead. Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 83 -------------------- 1 file changed, 83 deletions(-) delete mode 100644 test/Dapr.E2E.Test/Workflows/WorkflowTest.cs diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs deleted file mode 100644 index 5ed5f50ee..000000000 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ /dev/null @@ -1,83 +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.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapr.Client; -using FluentAssertions; -using Xunit; -using Dapr.Workflow; - -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.Fail("The logs were not able to found within the timeout"); - } - } - } -} From 0b89433a3fddeb96902ded22cd5f62274222c024 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 14 Oct 2024 18:18:36 -0500 Subject: [PATCH 15/69] Adding instance-based CreateInvokableHttpClient (#1319) This PR takes the implementation of the static method and puts it into the DaprClient instance, pulling from the existing apiTokenHeader on the instance to populate the daprApiToken, pulling the endpoint from the instance's httpEndpoint value and accepting only an appId argument so as to specify the ID of the Dapr app to connect to and place in the resulting URI. --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Client/DaprClient.cs | 26 +++++++++- src/Dapr.Client/DaprClientGrpc.cs | 25 +++++++++ ...lientTest.CreateInvokableHttpClientTest.cs | 52 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 test/Dapr.Client.Test/DaprClientTest.CreateInvokableHttpClientTest.cs diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 94a43b759..9f107578f 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -58,7 +58,7 @@ 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. /// /// /// @@ -448,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. diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index c0c0015e0..c70aef77b 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -450,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)); 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); + } + } +} From d1b6479921c8cf8553dfc5e8c6f74265bceb1907 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 15 Oct 2024 18:09:53 -0500 Subject: [PATCH 16/69] Fixed security advisory updates across dependencies (transitive and direct) (#1366) Migrating whole solution to Central Package Management - several package version upgrades to address security advisories and otherwise. --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 45 +++++++++++++++++++ .../GrpcServiceSample.csproj | 10 ++--- .../BulkPublishEventExample.csproj | 6 +-- .../PublishEventExample.csproj | 6 +-- .../ServiceInvocation.csproj | 6 +-- .../StateManagement/StateManagement.csproj | 6 +-- .../WorkflowUnitTest/WorkflowUnitTest.csproj | 10 ++--- properties/dapr_managed_netcore.props | 2 +- .../Dapr.Actors.Generators.csproj | 4 +- src/Dapr.Actors/Dapr.Actors.csproj | 2 +- src/Dapr.Client/Dapr.Client.csproj | 8 ++-- .../Dapr.Extensions.Configuration.csproj | 2 +- src/Dapr.Workflow/Dapr.Workflow.csproj | 4 +- src/Directory.Build.props | 2 +- ...r.Actors.AspNetCore.IntegrationTest.csproj | 12 ++--- .../Dapr.Actors.AspNetCore.Test.csproj | 14 +++--- .../CSharpSourceGeneratorVerifier.cs | 4 +- .../Dapr.Actors.Generators.Test.csproj | 16 +++---- test/Dapr.Actors.Test/Dapr.Actors.Test.csproj | 14 +++--- .../Dapr.AspNetCore.IntegrationTest.csproj | 35 +++++++++------ .../Dapr.AspNetCore.Test.csproj | 10 ++--- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 24 +++++----- .../DaprClientTest.InvokeMethodGrpcAsync.cs | 2 +- test/Dapr.Client.Test/StateApiTest.cs | 2 +- .../Dapr.E2E.Test.Actors.Generators.csproj | 10 ++--- .../Dapr.E2E.Test.App.Grpc.csproj | 2 +- .../Dapr.E2E.Test.App.csproj | 10 ++--- test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 14 +++--- .../Dapr.Extensions.Configuration.Test.csproj | 14 +++--- test/Directory.Build.props | 6 +-- 30 files changed, 178 insertions(+), 124 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..990b5aeb1 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,45 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 123763489..6084df013 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + 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/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/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/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 7163f4e0c..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/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj index a69f2d1a0..370d422f1 100644 --- a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -10,8 +10,8 @@ - - + + 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 c44d19f61..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 9a8b55c2f..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,16 +5,16 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index 435488c2c..2b1046e1a 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -24,7 +24,9 @@ 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() { @@ -78,4 +80,4 @@ protected override ParseOptions CreateParseOptions() return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); } } -} \ No newline at end of file +} diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 02aaf1bb3..91c7e8b42 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -13,17 +13,17 @@ - - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index d87ea3cd3..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.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index ed110191f..ace894a4f 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,19 +1,26 @@  - - - 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 + + + + + NU1903 + NU1903 + false + direct + diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index 32416dd8a..a76288891 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 diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index 06322f4d1..9a8d91c79 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 diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 4001e4b06..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] diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 2595fb006..0684a8db0 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -505,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/Dapr.E2E.Test.Actors.Generators.csproj b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj index 6ef9c009d..cb375af01 100644 --- 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 @@ -9,14 +9,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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/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/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index be3027269..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.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index d259f2ab1..0c7bce286 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,15 +1,15 @@  - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,7 +25,7 @@ - + \ No newline at end of file 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 From b148bc403f3b715947b5665cf7ac960fd6e9933f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 16 Oct 2024 00:21:52 -0500 Subject: [PATCH 17/69] Removes floating classes and introduces Dapr.Common project (#1365) Extracting classes out to common project --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 1 + all.sln | 12 +++--- .../ConfigurationApi/ConfigurationApi.csproj | 1 + .../DistributedLock/DistributedLock.csproj | 1 + src/Dapr.Actors/Dapr.Actors.csproj | 6 +-- src/Dapr.AspNetCore/Dapr.AspNetCore.csproj | 6 +-- src/Dapr.Client/Dapr.Client.csproj | 9 ++--- .../ArgumentVerifier.cs | 2 +- src/Dapr.Common/AssemblyInfo.cs | 40 +++++++++++++++++++ src/Dapr.Common/Dapr.Common.csproj | 14 +++++++ src/{Shared => Dapr.Common}/DaprDefaults.cs | 2 - .../DaprException.cs | 3 +- .../Dapr.Extensions.Configuration.csproj | 5 +-- src/Dapr.Workflow/Dapr.Workflow.csproj | 6 +-- .../Dapr.AspNetCore.IntegrationTest.csproj | 8 +--- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 1 + .../Dapr.Extensions.Configuration.Test.csproj | 1 + ...aprSecretStoreConfigurationProviderTest.cs | 1 + 18 files changed, 78 insertions(+), 41 deletions(-) rename src/{Shared => Dapr.Common}/ArgumentVerifier.cs (96%) create mode 100644 src/Dapr.Common/AssemblyInfo.cs create mode 100644 src/Dapr.Common/Dapr.Common.csproj rename src/{Shared => Dapr.Common}/DaprDefaults.cs (99%) rename src/{Dapr.Client => Dapr.Common}/DaprException.cs (96%) diff --git a/Directory.Packages.props b/Directory.Packages.props index 990b5aeb1..d85020770 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,6 +28,7 @@ + diff --git a/all.sln b/all.sln index 0425b00f2..1d5b011ca 100644 --- a/all.sln +++ b/all.sln @@ -118,7 +118,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{B445B19C-A925-4873-8CB7-8317898B6970}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -292,10 +292,10 @@ Global {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 - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.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 @@ -349,7 +349,7 @@ Global {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} - {7CA93D67-C551-430E-AA2C-BC64B77F7908} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} 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/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/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 37c73e0ed..54d3487b8 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -6,13 +6,9 @@ $(PackageTags);Actors - - - - - + diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index 54996e4bc..12b512fbb 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -5,17 +5,13 @@ This package contains the reference assemblies for developing services using Dapr and AspNetCore. - - - - - + diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index e091078c0..73f758a8f 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,10 +1,5 @@  - - - - - @@ -26,4 +21,8 @@ + + + + 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 29fd62ec4..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,6 +11,7 @@ + diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index af99e62d0..992baee73 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -17,13 +17,11 @@ - - - - + + \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index ace894a4f..b1b27e618 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -15,15 +15,9 @@ - - NU1903 - NU1903 - false - direct - - + diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index 9a8d91c79..f5d7d8c99 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -33,6 +33,7 @@ + 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 0c7bce286..0ea0adeb7 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -21,6 +21,7 @@ + diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index 9bac31352..74b66c3cb 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -16,6 +16,7 @@ using System.Net; using System.Threading.Tasks; using Dapr.Client; +using Dapr; using FluentAssertions; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; From 614a92b7e238a2603bfacb996ebd8ff83b561ba9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 16 Oct 2024 00:36:20 -0500 Subject: [PATCH 18/69] Extracted Protos out to common project (#1367) Protos pulled out to separate shared project Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ++ .../GrpcServiceSample.csproj | 1 + src/Dapr.Client/Dapr.Client.csproj | 16 ++--- src/Dapr.Protos/Dapr.Protos.csproj | 22 +++++++ .../Protos/dapr/proto/common/v1/common.proto | 4 +- .../dapr/proto/runtime}/v1/appcallback.proto | 16 ++--- .../Protos/dapr/proto/runtime}/v1/dapr.proto | 66 ++++++++++++------- .../Dapr.AspNetCore.IntegrationTest.csproj | 1 + .../Dapr.AspNetCore.Test.csproj | 1 + test/Dapr.Client.Test/Dapr.Client.Test.csproj | 1 + .../Dapr.Extensions.Configuration.Test.csproj | 1 + 11 files changed, 89 insertions(+), 47 deletions(-) create mode 100644 src/Dapr.Protos/Dapr.Protos.csproj rename src/{Dapr.Client => Dapr.Protos}/Protos/dapr/proto/common/v1/common.proto (98%) rename src/{Dapr.Client/Protos/dapr/proto/dapr => Dapr.Protos/Protos/dapr/proto/runtime}/v1/appcallback.proto (98%) rename src/{Dapr.Client/Protos/dapr/proto/dapr => Dapr.Protos/Protos/dapr/proto/runtime}/v1/dapr.proto (95%) diff --git a/all.sln b/all.sln index 1d5b011ca..1a5d78efb 100644 --- a/all.sln +++ b/all.sln @@ -118,6 +118,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera 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 @@ -292,6 +294,10 @@ Global {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 @@ -349,6 +355,7 @@ Global {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 diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 6084df013..2319f6a56 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 73f758a8f..7d74a7bb3 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,28 +1,20 @@  - - - - - - This package contains the reference assemblies for developing services using Dapr. + - - - - - + + - + 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 98% 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 4acf9159d..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 { 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 98% 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 a86040364..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. @@ -58,7 +58,7 @@ 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. @@ -185,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. @@ -203,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; @@ -310,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. 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 95% 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 4185fb391..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,7 +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/dapr/v1/appcallback.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "DaprProtos"; @@ -61,7 +61,7 @@ service Dapr { // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic // events from it. - rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream TopicEventRequest) {} + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream SubscribeTopicEventsResponseAlpha1) {} // Invokes binding data to specific output bindings rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} @@ -428,17 +428,17 @@ message BulkPublishResponseFailedEntry { // 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 responses. +// messages must be event processed responses. message SubscribeTopicEventsRequestAlpha1 { oneof subscribe_topic_events_request_type { - SubscribeTopicEventsInitialRequestAlpha1 initial_request = 1; - SubscribeTopicEventsResponseAlpha1 event_response = 2; + SubscribeTopicEventsRequestInitialAlpha1 initial_request = 1; + SubscribeTopicEventsRequestProcessedAlpha1 event_processed = 2; } } -// SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the -// details for subscribing to a topic via streaming. -message SubscribeTopicEventsInitialRequestAlpha1 { +// 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; @@ -456,9 +456,9 @@ message SubscribeTopicEventsInitialRequestAlpha1 { optional string dead_letter_topic = 4; } -// SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +// SubscribeTopicEventsRequestProcessedAlpha1 is the message containing the // subscription to a topic. -message SubscribeTopicEventsResponseAlpha1 { +message SubscribeTopicEventsRequestProcessedAlpha1 { // id is the unique identifier for the subscription request. string id = 1; @@ -466,6 +466,21 @@ message SubscribeTopicEventsResponseAlpha1 { 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 { // The name of the output binding to invoke. @@ -478,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. @@ -824,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. @@ -865,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 @@ -1047,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). @@ -1196,14 +1212,14 @@ message Job { // "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 * * * * + // 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 @@ -1258,4 +1274,4 @@ message DeleteJobRequest { // DeleteJobResponse is the message response to delete the job by name. message DeleteJobResponse { // Empty -} +} \ No newline at end of file diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index b1b27e618..d51dc70e8 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index a76288891..9135e63d4 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -21,6 +21,7 @@ + \ No newline at end of file diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index f5d7d8c99..f0bea601f 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -33,6 +33,7 @@ + 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 0ea0adeb7..ef6cfbcee 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -23,6 +23,7 @@ + From a8e1b7905deb24f0794143dd43d74322799460d4 Mon Sep 17 00:00:00 2001 From: Shubhdeep Singh Date: Thu, 17 Oct 2024 02:11:46 +0530 Subject: [PATCH 19/69] Improvement of the dotnet-contributing files (#1330) Add link about Dapr bot to contribution documentation Signed-off-by: Siri Varma Vegiraju --- CONTRIBUTING.md | 10 ++++++++++ .../en/dotnet-sdk-contributing/dotnet-contributing.md | 4 ++++ 2 files changed, 14 insertions(+) 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/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. From d538b26afdf6eca291e9389bd3c18883d03153b5 Mon Sep 17 00:00:00 2001 From: Ilias Date: Thu, 17 Oct 2024 23:39:39 +0100 Subject: [PATCH 20/69] Support case insensitive cloudevent payloads and forward cloudevent props s headers (#1153) * forward cloudevent props Signed-off-by: Ilias Politsopoulos * refactor middleware Signed-off-by: Ilias Politsopoulos * add cloud event property filters Signed-off-by: Ilias Politsopoulos * update string check Signed-off-by: Ilias Politsopoulos * forward cloudevent props Signed-off-by: Ilias Politsopoulos * refactor middleware Signed-off-by: Ilias Politsopoulos * add cloud event property filters Signed-off-by: Ilias Politsopoulos * update checks Signed-off-by: Ilias Politsopoulos --------- Signed-off-by: Whit Waldo Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../Controllers/SampleController.cs | 45 ++++-- .../AspNetCore/ControllerSample/Startup.cs | 8 +- .../CloudEventPropertyNames.cs | 9 ++ src/Dapr.AspNetCore/CloudEventsMiddleware.cs | 116 +++++++++++--- .../CloudEventsMiddlewareOptions.cs | 42 ++++- .../CloudEventsMiddlewareTest.cs | 144 ++++++++++++++++++ 6 files changed, 332 insertions(+), 32 deletions(-) create mode 100644 src/Dapr.AspNetCore/CloudEventPropertyNames.cs 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/Startup.cs b/examples/AspNetCore/ControllerSample/Startup.cs index 64cfba512..ddc6d1c5f 100644 --- a/examples/AspNetCore/ControllerSample/Startup.cs +++ b/examples/AspNetCore/ControllerSample/Startup.cs @@ -11,8 +11,11 @@ // limitations under the License. // ------------------------------------------------------------------------ + +using Dapr; using Dapr.AspNetCore; + namespace ControllerSample { using Microsoft.AspNetCore.Builder; @@ -63,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/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/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() { From 453da80f61196a16a62e05a5a700ba4f690067b1 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 18 Oct 2024 02:56:12 -0500 Subject: [PATCH 21/69] Updating actor serialization documentation (#1371) * Changed headers, updated introduction to reflect the difference in serialization between either type and added a brief section to detail the use of System.Text.Json for weakly-typed Dapr actor clients and to point to official documentation on it --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../dotnet-actors-serialization.md | 278 +++++++++++++++++- 1 file changed, 263 insertions(+), 15 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md index abbeb437d..787a7e41f 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md @@ -5,15 +5,263 @@ linkTitle: "Actor serialization" weight: 300000 description: Necessary steps to serialize your types using remoted Actors in .NET --- +# Actor Serialization -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. +The Dapr actor package enables you to use Dapr virtual actors within a .NET application with either a weakly- or strongly-typed client. Each utilizes a different serialization approach. This document will review the differences and convey a few key ground rules to understand in either scenario. -# 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. +Please be advised that it is not a supported scenario to use the weakly- or strongly typed actor clients interchangeably because of these different serialization approaches. The data persisted using one Actor client will not be accessible using the other Actor client, so it is important to pick one and use it consistently throughout your application. -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). +## Weakly-typed Dapr Actor client +In this section, you will learn how to configure your C# types so they are properly serialized and deserialized at runtime when using a weakly-typed actor client. These clients use string-based names of methods with request and response payloads that are serialized using the System.Text.Json serializer. Please note that 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/tree/main/src/libraries/System.Text.Json). -## Serializable Types +When using the weakly-typed Dapr Actor client to invoke methods from your various actors, it's not necessary to independently serialize or deserialize the method payloads as this will happen transparently on your behalf by the SDK. + +The client will use the latest version of System.Text.Json available for the version of .NET you're building against and serialization is subject to all the inherent capabilities provided in the [associated .NET documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview). + +The serializer will be configured to use the `JsonSerializerOptions.Web` [default options](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/configure-options?pivots=dotnet-8-0#web-defaults-for-jsonserializeroptions) unless overridden with a custom options configuration which means the following are applied: +- Deserialization of the property name is performed in a case-insensitive manner +- Serialization of the property name is performed using [camel casing](https://en.wikipedia.org/wiki/Camel_case) unless the property is overridden with a `[JsonPropertyName]` attribute +- Deserialization will read numeric values from number and/or string values + +### Basic Serialization +In the following example, we present a simple class named Doodad though it could just as well be a record as well. + +```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: + +```json +{"id": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5} +``` + +### Override Serialized Property Name +The default property names can be overridden by applying the `[JsonPropertyName]` attribute to desired properties. + +Generally, this isn't going to be necessary for types you're persisting to the actor state as you're not intended to read or write them independent of Dapr-associated functionality, but +the following is provided just to clearly illustrate that it's possible. + +#### Override Property Names on Classes +Here's an example demonstrating the use of `JsonPropertyName` to change the name for the first property following serialization. Note that the last usage of `JsonPropertyName` on the `Count` property +matches what it would be expected to serialize to. This is largely just to demonstrate that applying this attribute won't negatively impact anything - in fact, it might be preferable if you later +decide to change the default serialization options but still need to consistently access the properties previously serialized before that change as `JsonPropertyName` will override those options. + +```csharp +public class Doodad +{ + [JsonPropertyName("identifier")] + public Guid Id { get; set; } + public string Name { get; set; } + [JsonPropertyName("count")] + public int Count { get; set; } +} +``` + +This would serialize to the following: + +```json +{"identifier": "a06ced64-4f42-48ad-84dd-46ae6a7e333d", "name": "DoodadName", "count": 5} +``` + +#### Override Property Names on Records +Let's try doing the same thing with a record from C# 12 or later: + +```csharp +public record Thingy(string Name, [JsonPropertyName("count")] int Count); +``` + +Because the argument passed in a primary constructor (introduced in C# 12) can be applied to either a property or field within a record, using the `[JsonPropertyName]` attribute may +require specifying that you intend the attribute to apply to a property and not a field in some ambiguous cases. Should this be necessary, you'd indicate as much in the primary constructor with: + +```csharp +public record Thingy(string Name, [property: JsonPropertyName("count")] int Count); +``` + +If `[property: ]` is applied to the `[JsonPropertyName]` attribute where it's not necessary, it will not negatively impact serialization or deserialization as the operation will +proceed normally as though it were a property (as it typically would if not marked as such). + +### Enumeration types +Enumerations, including flat enumerations are serializable to JSON, but the value persisted may surprise you. Again, it's not expected that the developer should ever engage +with the serialized data independently of Dapr, but the following information may at least help in diagnosing why a seemingly mild version migration isn't working as expected. + +Take the following `enum` type providing the various seasons in the year: + +```csharp +public enum Season +{ + Spring, + Summer, + Fall, + Winter +} +``` + +We'll go ahead and use a separate demonstration type that references our `Season` and simultaneously illustrate how this works with records: + +```csharp +public record Engagement(string Name, Season TimeOfYear); +``` + +Given the following initialized instance: + +```csharp +var myEngagement = new Engagement("Ski Trip", Season.Winter); +``` + +This would serialize to the following JSON: +```json +{"name": "Ski Trip", "season": 3} +``` + +That might be unexpected that our `Season.Winter` value was represented as a `3`, but this is because the serializer is going to automatically use numeric representations +of the enum values starting with zero for the first value and incrementing the numeric value for each additional value available. Again, if a migration were taking place and +a developer had flipped the order of the enums, this would affect a breaking change in your solution as the serialized numeric values would point to different values when deserialized. + +Rather, there is a `JsonConverter` available with `System.Text.Json` that will instead opt to use a string-based value instead of the numeric value. The `[JsonConverter]` attribute needs +to be applied to be enum type itself to enable this, but will then be realized in any downstream serialization or deserialization operation that references the enum. + +```csharp +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Season +{ + Spring, + Summer, + Fall, + Winter +} +``` + +Using the same values from our `myEngagement` instance above, this would produce the following JSON instead: + +```json +{"name": "Ski Trip", "season": "Winter"} +``` + +As a result, the enum members can be shifted around without fear of introducing errors during deserialization. + +#### Custom Enumeration Values + +The System.Text.Json serialization platform doesn't, out of the box, support the use of `[EnumMember]` to allow you to change the value of enum that's used during serialization or deserialization, but +there are scenarios where this could be useful. Again, assume that you're tasking with refactoring the solution to apply some better names to your various +enums. You're using the `JsonStringEnumConverter` detailed above so you're saving the name of the enum to value instead of a numeric value, but if you change +the enum name, that will introduce a breaking change as the name will no longer match what's in state. + +Do note that if you opt into using this approach, you should decorate all your enum members with the `[EnumMeber]` attribute so that the values are consistently applied for each enum value instead +of haphazardly. Nothing will validate this at build or runtime, but it is considered a best practice operation. + +How can you specify the precise value persisted while still changing the name of the enum member in this scenario? Use a custom `JsonConverter` with an extension method that can pull the value +out of the attached `[EnumMember]` attributes where provided. Add the following to your solution: + +```csharp +public sealed class EnumMemberJsonConverter : JsonConverter where T : struct, Enum +{ + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Get the string value from the JSON reader + var value = reader.GetString(); + + // Loop through all the enum values + foreach (var enumValue in Enum.GetValues()) + { + // Get the value from the EnumMember attribute, if any + var enumMemberValue = GetValueFromEnumMember(enumValue); + + // If the values match, return the enum value + if (value == enumMemberValue) + { + return enumValue; + } + } + + // If no match found, throw an exception + throw new JsonException($"Invalid value for {typeToConvert.Name}: {value}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // Get the value from the EnumMember attribute, if any + var enumMemberValue = GetValueFromEnumMember(value); + + // Write the value to the JSON writer + writer.WriteStringValue(enumMemberValue); + } + + private static string GetValueFromEnumMember(T value) + { + MemberInfo[] member = typeof(T).GetMember(value.ToString(), BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public); + if (member.Length == 0) + return value.ToString(); + object[] customAttributes = member.GetCustomAttributes(typeof(EnumMemberAttribute), false); + if (customAttributes.Length != 0) + { + EnumMemberAttribute enumMemberAttribute = (EnumMemberAttribute)customAttributes; + if (enumMemberAttribute != null && enumMemberAttribute.Value != null) + return enumMemberAttribute.Value; + } + return value.ToString(); + } +} +``` + +Now let's add a sample enumerator. We'll set a value that uses the lower-case version of each enum member to demonstrate this. Don't forget to decorate the enum with the `JsonConverter` +attribute and reference our custom converter in place of the numeral-to-string converter used in the last section. + +```csharp +[JsonConverter(typeof(EnumMemberJsonConverter))] +public enum Season +{ + [EnumMember(Value="spring")] + Spring, + [EnumMember(Value="summer")] + Summer, + [EnumMember(Value="fall")] + Fall, + [EnumMember(Value="winter")] + Winter +} +``` + +Let's use our sample record from before. We'll also add a `[JsonPropertyName]` attribute just to augment the demonstration: +```csharp +public record Engagement([property: JsonPropertyName("event")] string Name, Season TimeOfYear); +``` + +And finally, let's initialize a new instance of this: + +```csharp +var myEngagement = new Engagement("Conference", Season.Fall); +``` + +This time, serialization will take into account the values from the attached `[EnumMember]` attribute providing us a mechanism to refactor our application without necessitating +a complex versioning scheme for our existing enum values in the state. + +```json +{"event": "Conference", "season": "fall"} +``` + +## Strongly-typed Dapr Actor client +In this section, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime when using a strongly-typed actor client. These clients are implemented using .NET interfaces and are not compatible with Dapr Actors written using other languages. + +This actor client serializes data using an engine called the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) which converts your C# types to and from XML documents. 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). + +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. + +### 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 @@ -23,14 +271,14 @@ There are several important considerations to keep in mind when using the Data C - 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? +### 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 +#### .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. @@ -153,7 +401,7 @@ When this is serialized, because we're changing the names of the serialized memb ``` -#### Classes in C# 12 - Primary Constructors +##### 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. @@ -198,7 +446,7 @@ public class Doodad(ILogger _logger) } ``` -### .NET Structs +#### .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 @@ -210,7 +458,7 @@ public struct Doodad } ``` -### .NET Records +#### .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: @@ -238,7 +486,7 @@ public record Doodad( [property: DataMember] int Count) ``` -### Supported Primitive Types +#### 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) @@ -267,7 +515,7 @@ There are additional types that aren't actually primitives but have similar buil 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 +#### 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. @@ -283,15 +531,15 @@ public enum Colors } ``` -### Collection Types +#### 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 +#### 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 +#### 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. From e07a12e02e83a62d99721a0119b3bb25e2daf1cb Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 18 Oct 2024 03:20:01 -0500 Subject: [PATCH 22/69] Prioritize retrieval of environment variables from IConfiguration instead of directly (#1363) * Implemented against Dapr.Client.AspNetCore and Dapr.Client Signed-off-by: Whit Waldo * SImplified DaprWorkflow DI registration and updated to use IConfiguration preference. Needs testing. Signed-off-by: Whit Waldo * Added missing copyright header Signed-off-by: Whit Waldo * Updated actor registration to prefer the updated IConfiguration-based approach for pulling the HTTP endpoint and API token Signed-off-by: Whit Waldo * Adopted accepted proposal's guidelines for favoring different environment variables for determining the sidecar endpoint. Added notes to explain this in the code going forward. Signed-off-by: Whit Waldo * Made some lines a little more concise, added hostname default to DaprDefaults to use when building endpoints. Signed-off-by: Whit Waldo * Fixed and updated unit tests Signed-off-by: Whit Waldo * Updated to put endpoint resolution mechanism in DaprDefaults within Dapr.Common - updating projects and unit tests Signed-off-by: Whit Waldo * Updated packages to fix security advisory https://github.com/advisories/GHSA-447r-wph3-92pm Signed-off-by: Whit Waldo * Updated Workflow builder to use DaprDefaults with IConfiguration Signed-off-by: Whit Waldo * Updating global.json Signed-off-by: Whit Waldo * Tweaked global.json comment Signed-off-by: Whit Waldo * Adding braces per nit Signed-off-by: Whit Waldo * Consolidated both registration extension methods to remove duplication Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 3 + all.sln | 7 + global.json | 6 +- .../ActorsServiceCollectionExtensions.cs | 28 +- .../Dapr.Actors.AspNetCore.csproj | 1 + src/Dapr.Actors/Client/ActorProxyOptions.cs | 2 +- src/Dapr.Actors/Dapr.Actors.csproj | 1 + .../Runtime/ActorRuntimeOptions.cs | 22 +- .../DaprAuthenticationOptions.cs | 3 +- .../DaprServiceCollectionExtensions.cs | 101 +-- src/Dapr.Client/DaprClient.cs | 11 +- src/Dapr.Client/DaprClientBuilder.cs | 2 +- src/Dapr.Client/InvocationHandler.cs | 2 +- src/Dapr.Common/Dapr.Common.csproj | 2 + src/Dapr.Common/DaprDefaults.cs | 131 ++-- .../DaprWorkflowClientBuilderFactory.cs | 95 +++ .../WorkflowServiceCollectionExtensions.cs | 135 +--- .../Dapr.Actors.Generators.Test.csproj | 1 + .../DaprServiceCollectionExtensionsTest.cs | 25 +- test/Dapr.Common.Test/Dapr.Common.Test.csproj | 23 + test/Dapr.Common.Test/DaprDefaultTest.cs | 582 ++++++++++++++++++ 21 files changed, 918 insertions(+), 265 deletions(-) create mode 100644 src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs create mode 100644 test/Dapr.Common.Test/Dapr.Common.Test.csproj create mode 100644 test/Dapr.Common.Test/DaprDefaultTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index d85020770..842c82aa8 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,6 +5,7 @@ + @@ -26,6 +27,7 @@ + @@ -39,6 +41,7 @@ + diff --git a/all.sln b/all.sln index 1a5d78efb..85ed848a4 100644 --- a/all.sln +++ b/all.sln @@ -122,6 +122,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Protos", "src\Dapr.Pro EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{B445B19C-A925-4873-8CB7-8317898B6970}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -302,6 +304,10 @@ Global {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 + {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -357,6 +363,7 @@ Global {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/global.json b/global.json index 980f4652d..fe53f92ae 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { - "_comment": "This policy allows the 7.0.101 SDK or patches in that family.", + "_comment": "This policy allows the 8.0.100 SDK or patches in that family.", "sdk": { - "version": "7.0.101", - "rollForward": "latestMajor" + "version": "8.0.100", + "rollForward": "minor" } } \ No newline at end of file diff --git a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs index a7f5d04a2..11f05f4c1 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.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,9 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable + using System; +using Dapr; using Dapr.Actors.Client; using Dapr.Actors.Runtime; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -30,12 +34,9 @@ public static class ActorsServiceCollectionExtensions /// /// The . /// A delegate used to configure actor options and register actor types. - public static void AddActors(this IServiceCollection services, Action configure) + public static void AddActors(this IServiceCollection? services, Action? configure) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } + ArgumentNullException.ThrowIfNull(services, nameof(services)); // Routing and health checks are required dependencies. services.AddRouting(); @@ -45,6 +46,8 @@ public static void AddActors(this IServiceCollection services, Action(s => { var options = s.GetRequiredService>().Value; + ConfigureActorOptions(s, options); + var loggerFactory = s.GetRequiredService(); var activatorFactory = s.GetRequiredService(); var proxyFactory = s.GetRequiredService(); @@ -54,6 +57,8 @@ public static void AddActors(this IServiceCollection services, Action(s => { var options = s.GetRequiredService>().Value; + ConfigureActorOptions(s, options); + var factory = new ActorProxyFactory() { DefaultOptions = @@ -72,5 +77,16 @@ public static void AddActors(this IServiceCollection services, Action(configure); } } + + private static void ConfigureActorOptions(IServiceProvider serviceProvider, ActorRuntimeOptions options) + { + var configuration = serviceProvider.GetService(); + options.DaprApiToken = !string.IsNullOrWhiteSpace(options.DaprApiToken) + ? options.DaprApiToken + : DaprDefaults.GetDefaultDaprApiToken(configuration); + options.HttpEndpoint = !string.IsNullOrWhiteSpace(options.HttpEndpoint) + ? options.HttpEndpoint + : DaprDefaults.GetDefaultHttpEndpoint(); + } } } diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index 82c5863db..ef57e76fe 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -11,5 +11,6 @@ + diff --git a/src/Dapr.Actors/Client/ActorProxyOptions.cs b/src/Dapr.Actors/Client/ActorProxyOptions.cs index 665a1dced..2afce852c 100644 --- a/src/Dapr.Actors/Client/ActorProxyOptions.cs +++ b/src/Dapr.Actors/Client/ActorProxyOptions.cs @@ -45,7 +45,7 @@ public JsonSerializerOptions JsonSerializerOptions /// /// The Dapr Api Token that is added to the header for all requests. /// - public string DaprApiToken { get; set; } = DaprDefaults.GetDefaultDaprApiToken(); + public string DaprApiToken { get; set; } = DaprDefaults.GetDefaultDaprApiToken(null); /// /// Gets or sets the HTTP endpoint URI used to communicate with the Dapr sidecar. diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 54d3487b8..bcb8d830f 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs index 62eaceea6..21c302018 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable + using System; using System.Text.Json; @@ -34,7 +36,7 @@ public sealed class ActorRuntimeOptions }; private bool useJsonSerialization = false; private JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; - private string daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + private string daprApiToken = string.Empty; private int? remindersStoragePartitions = null; /// @@ -180,19 +182,14 @@ public JsonSerializerOptions JsonSerializerOptions set { - if (value is null) - { - throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorRuntimeOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); - } - - this.jsonSerializerOptions = value; + this.jsonSerializerOptions = value ?? throw new ArgumentNullException(nameof(JsonSerializerOptions), $"{nameof(ActorRuntimeOptions)}.{nameof(JsonSerializerOptions)} cannot be null"); } } /// /// The to add to the headers in requests to Dapr runtime /// - public string DaprApiToken + public string? DaprApiToken { get { @@ -201,12 +198,7 @@ public string DaprApiToken set { - if (value is null) - { - throw new ArgumentNullException(nameof(DaprApiToken), $"{nameof(ActorRuntimeOptions)}.{nameof(DaprApiToken)} cannot be null"); - } - - this.daprApiToken = value; + this.daprApiToken = value ?? throw new ArgumentNullException(nameof(DaprApiToken), $"{nameof(ActorRuntimeOptions)}.{nameof(DaprApiToken)} cannot be null"); } } @@ -241,6 +233,6 @@ public int? RemindersStoragePartitions /// corresponding environment variables. /// /// - public string HttpEndpoint { get; set; } = DaprDefaults.GetDefaultHttpEndpoint(); + public string? HttpEndpoint { get; set; } } } diff --git a/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs b/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs index 4e073d06c..b12d4d14e 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationOptions.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using Microsoft.AspNetCore.Authentication; namespace Dapr.AspNetCore @@ -29,6 +28,6 @@ public class DaprAuthenticationOptions : AuthenticationSchemeOptions /// Gets or sets the App API token. /// By default, the token will be read from the APP_API_TOKEN environment variable. /// - public string Token { get; set; } = DaprDefaults.GetDefaultAppApiToken(); + public string Token { get; set; } = DaprDefaults.GetDefaultAppApiToken(null); } } diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 388015b80..52e9110be 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.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,55 +11,76 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection -{ - using System; - using System.Linq; - using Dapr.Client; - using Extensions; +#nullable enable + +namespace Microsoft.Extensions.DependencyInjection; + +using System; +using Dapr; +using Dapr.Client; +using Extensions; +using Configuration; +/// +/// Provides extension methods for . +/// +public static class DaprServiceCollectionExtensions +{ /// - /// Provides extension methods for . + /// 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. /// - public static class DaprServiceCollectionExtensions + /// The . + /// + public static void AddDaprClient(this IServiceCollection services, Action? configure = null) { - /// - /// 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 = null) + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.TryAddSingleton(serviceProvider => { - ArgumentNullException.ThrowIfNull(services, nameof(services)); + var builder = CreateDaprClientBuilder(serviceProvider); + configure?.Invoke(builder); + return builder.Build(); + }); + } - services.TryAddSingleton(_ => - { - var builder = new DaprClientBuilder(); - configure?.Invoke(builder); + /// + /// 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)); - return builder.Build(); - }); - } - - /// - /// 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) + services.TryAddSingleton(serviceProvider => { - ArgumentNullException.ThrowIfNull(services, nameof(services)); + var builder = CreateDaprClientBuilder(serviceProvider); + configure?.Invoke(serviceProvider, builder); + return builder.Build(); + }); + } + + private static DaprClientBuilder CreateDaprClientBuilder(IServiceProvider serviceProvider) + { + var builder = new DaprClientBuilder(); + var configuration = serviceProvider.GetService(); + + // Set the HTTP endpoint, if provided, else use the default endpoint + builder.UseHttpEndpoint(DaprDefaults.GetDefaultHttpEndpoint(configuration)); - services.TryAddSingleton(serviceProvider => - { - var builder = new DaprClientBuilder(); - configure?.Invoke(serviceProvider, builder); + // Set the gRPC endpoint, if provided + builder.UseGrpcEndpoint(DaprDefaults.GetDefaultGrpcEndpoint(configuration)); - return builder.Build(); - }); + // Set the API token, if provided + var apiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + if (!string.IsNullOrWhiteSpace(apiToken)) + { + builder.UseDaprApiToken(apiToken); } + + return builder; } } diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 9f107578f..43c640a69 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -139,17 +139,14 @@ public static HttpClient CreateInvokeHttpClient(string appId = null, string dapr public static CallInvoker CreateInvocationInvoker(string appId, string daprEndpoint = null, string daprApiToken = null) { var channel = GrpcChannel.ForAddress(daprEndpoint ?? DaprDefaults.GetDefaultGrpcEndpoint()); - return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken())); + return channel.Intercept(new InvocationInterceptor(appId, daprApiToken ?? DaprDefaults.GetDefaultDaprApiToken(null))); } internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) { - if (string.IsNullOrWhiteSpace(apiToken)) - { - return null; - } - - return new KeyValuePair("dapr-api-token", apiToken); + return string.IsNullOrWhiteSpace(apiToken) + ? null + : new KeyValuePair("dapr-api-token", apiToken); } /// diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 50a4979d1..68315c45b 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -40,7 +40,7 @@ public DaprClientBuilder() }; this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(null); } // property exposed for testing purposes diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index 1b55436aa..36fd6b77f 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -51,7 +51,7 @@ public class InvocationHandler : DelegatingHandler public InvocationHandler() { this.parsedEndpoint = new Uri(DaprDefaults.GetDefaultHttpEndpoint(), UriKind.Absolute); - this.apiToken = DaprDefaults.GetDefaultDaprApiToken(); + this.apiToken = DaprDefaults.GetDefaultDaprApiToken(null); } /// diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index ea3e8ae84..31af3952c 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -9,6 +9,8 @@ + + diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs index 575a3c148..85a4b18c8 100644 --- a/src/Dapr.Common/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Configuration; + namespace Dapr { internal static class DaprDefaults @@ -20,82 +22,111 @@ internal static class DaprDefaults private static string daprApiToken = string.Empty; private static string appApiToken = string.Empty; + public const string DaprApiTokenName = "DAPR_API_TOKEN"; + public const string AppApiTokenName = "APP_API_TOKEN"; + public const string DaprHttpEndpointName = "DAPR_HTTP_ENDPOINT"; + public const string DaprHttpPortName = "DAPR_HTTP_PORT"; + public const string DaprGrpcEndpointName = "DAPR_GRPC_ENDPOINT"; + public const string DaprGrpcPortName = "DAPR_GRPC_PORT"; + + public const string DefaultDaprScheme = "http"; + public const string DefaultDaprHost = "localhost"; + public const int DefaultHttpPort = 3500; + public const int DefaultGrpcPort = 50001; + /// /// Get the value of environment variable DAPR_API_TOKEN /// + /// The optional to pull the value from. /// The value of environment variable DAPR_API_TOKEN - public static string GetDefaultDaprApiToken() - { - // Lazy-init is safe because this is just populating the default - // We don't plan to support the case where the user changes environment variables - // for a running process. - if (string.IsNullOrEmpty(daprApiToken)) - { - // Treat empty the same as null since it's an environment variable - var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return daprApiToken; - } + public static string GetDefaultDaprApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, DaprApiTokenName) ?? string.Empty; /// /// Get the value of environment variable APP_API_TOKEN /// + /// The optional to pull the value from. /// The value of environment variable APP_API_TOKEN - public static string GetDefaultAppApiToken() - { - if (string.IsNullOrEmpty(appApiToken)) - { - var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return appApiToken; - } + public static string GetDefaultAppApiToken(IConfiguration? configuration) => + GetResourceValue(configuration, AppApiTokenName) ?? string.Empty; /// /// Get the value of HTTP endpoint based off environment variables /// + /// The optional to pull the value from. /// The value of HTTP endpoint based off environment variables - public static string GetDefaultHttpEndpoint() + public static string GetDefaultHttpEndpoint(IConfiguration? configuration = null) { - if (string.IsNullOrEmpty(httpEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - httpEndpoint = endpoint; - return httpEndpoint; - } - - var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); - port = string.IsNullOrEmpty(port) ? "3500" : port; - httpEndpoint = $"http://127.0.0.1:{port}"; - } + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprHttpEndpointName); + var port = GetResourceValue(configuration, DaprHttpPortName); + + //Use the default HTTP port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultHttpPort : int.Parse(port); - return httpEndpoint; + return BuildEndpoint(endpoint, parsedGrpcPort.Value); } /// /// Get the value of gRPC endpoint based off environment variables /// + /// The optional to pull the value from. /// The value of gRPC endpoint based off environment variables - public static string GetDefaultGrpcEndpoint() + public static string GetDefaultGrpcEndpoint(IConfiguration? configuration = null) { - if (string.IsNullOrEmpty(grpcEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - grpcEndpoint = endpoint; - return grpcEndpoint; - } + //Prioritize pulling from the IConfiguration and fallback to the environment variable if not populated + var endpoint = GetResourceValue(configuration, DaprGrpcEndpointName); + var port = GetResourceValue(configuration, DaprGrpcPortName); + + //Use the default gRPC port if we're unable to retrieve/parse the provided port + int? parsedGrpcPort = string.IsNullOrWhiteSpace(port) ? DefaultGrpcPort : int.Parse(port); + + return BuildEndpoint(endpoint, parsedGrpcPort.Value); + } + + /// + /// Builds the Dapr endpoint. + /// + /// The endpoint value. + /// The endpoint port value, whether pulled from configuration/envvar or the default. + /// A constructed endpoint value. + private static string BuildEndpoint(string? endpoint, int endpointPort) + { + var endpointBuilder = new UriBuilder { Scheme = DefaultDaprScheme, Host = DefaultDaprHost }; //Port depends on endpoint - var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - port = string.IsNullOrEmpty(port) ? "50001" : port; - grpcEndpoint = $"http://127.0.0.1:{port}"; + if (!string.IsNullOrWhiteSpace(endpoint)) //If the endpoint is set, it doesn't matter if the port is + { + //Extract the scheme, host and port from the endpoint and replace defaults + var uri = new Uri(endpoint); + endpointBuilder.Scheme = uri.Scheme; + endpointBuilder.Host = uri.Host; + endpointBuilder.Port = uri.Port; } + else + { + //Should only set the port if the endpoint isn't populated + endpointBuilder.Port = endpointPort; + } + + return endpointBuilder.ToString(); + } + + /// + /// Retrieves the specified value prioritizing pulling it from , falling back + /// to an environment variable, and using an empty string as a default. + /// + /// An instance of an . + /// The name of the value to retrieve. + /// The value of the resource. + private static string? GetResourceValue(IConfiguration? configuration, string name) + { + //Attempt to retrieve first from the configuration + var configurationValue = configuration?[name]; + if (configurationValue is not null) + return configurationValue; - return grpcEndpoint; + //Fall back to the environment variable with the same name or default to an empty string + return Environment.GetEnvironmentVariable(name); } } } diff --git a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs new file mode 100644 index 000000000..7a854cf05 --- /dev/null +++ b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Net.Http; +using Grpc.Net.Client; +using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +#nullable enable + +namespace Dapr.Workflow; + +/// +/// A factory for building a . +/// +internal sealed class DaprWorkflowClientBuilderFactory +{ + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IServiceCollection _services; + + /// + /// Constructor used to inject the required types into the factory. + /// + public DaprWorkflowClientBuilderFactory(IConfiguration configuration, IHttpClientFactory httpClientFactory, IServiceCollection services) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + _services = services; + } + + /// + /// Responsible for building the client itself. + /// + /// + public void CreateClientBuilder(Action configure) + { + _services.AddDurableTaskClient(builder => + { + var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + + var httpClient = _httpClientFactory.CreateClient(); + + if (!string.IsNullOrWhiteSpace(apiToken)) + { + httpClient.DefaultRequestHeaders.Add( "Dapr-Api-Token", apiToken); + } + + builder.UseGrpc(GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + builder.RegisterDirectly(); + }); + + _services.AddDurableTaskWorker(builder => + { + WorkflowRuntimeOptions options = new(); + configure?.Invoke(options); + + var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + + if (!string.IsNullOrEmpty(grpcEndpoint)) + { + var httpClient = _httpClientFactory.CreateClient(); + + if (!string.IsNullOrWhiteSpace(apiToken)) + { + httpClient.DefaultRequestHeaders.Add("Dapr-Api-Token", apiToken); + } + + builder.UseGrpc( + GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + } + else + { + builder.UseGrpc(); + } + + builder.AddTasks(registry => options.AddWorkflowsAndActivitiesToRegistry(registry)); + }); + } +} diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index ca514f221..3c19583aa 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -14,20 +14,14 @@ namespace Dapr.Workflow { using System; - using Grpc.Net.Client; - using Microsoft.DurableTask.Client; - using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; - using System.Net.Http; - using Dapr; /// /// Contains extension methods for using Dapr Workflow with dependency injection. /// public static class WorkflowServiceCollectionExtensions { - /// /// Adds Dapr Workflow support to the service collection. /// @@ -43,6 +37,7 @@ public static IServiceCollection AddDaprWorkflow( } serviceCollection.TryAddSingleton(); + serviceCollection.AddHttpClient(); #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient serviceCollection.TryAddSingleton(); @@ -50,135 +45,17 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.AddHostedService(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); - serviceCollection.AddDaprWorkflowClient(); - + serviceCollection.AddOptions().Configure(configure); - serviceCollection.AddDurableTaskWorker(builder => + serviceCollection.AddSingleton(c => { - WorkflowRuntimeOptions options = new(); - configure?.Invoke(options); - - if (TryGetGrpcAddress(out string address)) - { - var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - if (!string.IsNullOrEmpty(daprApiToken)) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); - builder.UseGrpc(CreateChannel(address, client)); - } - else - { - builder.UseGrpc(address); - } - - } - else - { - builder.UseGrpc(); - } - - builder.AddTasks(registry => options.AddWorkflowsAndActivitiesToRegistry(registry)); + var factory = c.GetRequiredService(); + factory.CreateClientBuilder(configure); + return new object(); //Placeholder as actual registration is performed inside factory }); return serviceCollection; } - - /// - /// Adds Dapr Workflow client support to the service collection. - /// - /// - /// Use this extension method if you want to use in your app - /// but don't wish to define any workflows or activities. - /// - /// The . - public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection services) - { - services.TryAddSingleton(); - services.AddDurableTaskClient(builder => - { - if (TryGetGrpcAddress(out string address)) - { - var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - if (!string.IsNullOrEmpty(daprApiToken)) - { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); - builder.UseGrpc(CreateChannel(address, client)); - } - else - { - builder.UseGrpc(address); - } - - } - else - { - builder.UseGrpc(); - } - - builder.RegisterDirectly(); - }); - - return services; - } - - static bool TryGetGrpcAddress(out string address) - { - // TODO: Ideally we should be using DaprDefaults.cs for this. However, there are two blockers: - // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows - // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. - // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. - var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - if (!String.IsNullOrEmpty(daprEndpoint)) { - address = daprEndpoint; - return true; - } - - var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - if (int.TryParse(daprPortStr, out int daprGrpcPort)) - { - // There is a bug in the Durable Task SDK that requires us to change the format of the address - // depending on the version of .NET that we're targeting. For now, we work around this manually. -#if NET6_0_OR_GREATER - address = $"http://localhost:{daprGrpcPort}"; -#else - address = $"localhost:{daprGrpcPort}"; -#endif - return true; - } - - address = string.Empty; - return false; - } - - static GrpcChannel CreateChannel(string address, HttpClient client) - { - - GrpcChannelOptions options = new() { HttpClient = client}; - var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - if (!String.IsNullOrEmpty(daprEndpoint)) { - return GrpcChannel.ForAddress(daprEndpoint, options); - } - - var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - if (int.TryParse(daprPortStr, out int daprGrpcPort)) - { - // If there is no address passed in, we default to localhost - if (String.IsNullOrEmpty(address)) - { - // There is a bug in the Durable Task SDK that requires us to change the format of the address - // depending on the version of .NET that we're targeting. For now, we work around this manually. - #if NET6_0_OR_GREATER - address = $"http://localhost:{daprGrpcPort}"; - #else - address = $"localhost:{daprGrpcPort}"; - #endif - } - - } - return GrpcChannel.ForAddress(address, options); - } } } diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 91c7e8b42..80a79cafe 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -27,6 +27,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index a82948cf3..4a340e22a 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.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,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; +#nullable enable + +using System; using System.Text.Json; using Dapr.Client; using Microsoft.Extensions.DependencyInjection; @@ -43,15 +45,16 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce() var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); + Assert.NotNull(daprClient); + Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); } [Fact] public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() { - + var services = new ServiceCollection(); services.AddSingleton(); services.AddDaprClient((provider, builder) => @@ -66,13 +69,15 @@ public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() }); var serviceProvider = services.BuildServiceProvider(); - - DaprClientGrpc client = serviceProvider.GetRequiredService() as DaprClientGrpc; - + + 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); + Assert.NotNull(client); + Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); } + #if NET8_0_OR_GREATER [Fact] public void AddDaprClient_WithKeyedServices() @@ -90,7 +95,7 @@ public void AddDaprClient_WithKeyedServices() Assert.NotNull(daprClient); } #endif - + private class TestConfigurationProvider { public bool GetCaseSensitivity() => false; diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj new file mode 100644 index 000000000..6e34c3a7a --- /dev/null +++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj @@ -0,0 +1,23 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/test/Dapr.Common.Test/DaprDefaultTest.cs b/test/Dapr.Common.Test/DaprDefaultTest.cs new file mode 100644 index 000000000..ef4d0da3c --- /dev/null +++ b/test/Dapr.Common.Test/DaprDefaultTest.cs @@ -0,0 +1,582 @@ +using System; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Dapr.Common.Test; + +public class DaprDefaultTest +{ + [Fact] + public void ShouldBuildHttpEndpointAndPortUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "test_DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointAndPortUsingConfiguration() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "test_DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingConfiguration() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingOnlyPortConfiguration() + { + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:2569/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingEnvVarValues() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "http://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://dapr.io:80/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildHttpEndpointUsingMixedValues() + { + const string endpointVarName = "test_DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2569"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + configurationBuilder.AddEnvironmentVariables("test_"); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("https://dapr.io:443/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToEmptyHttpEndpoint() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:3500/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToLocalhostWithPort() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, "7256"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:7256/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldDefaultToLocalhostWithDefaultPort() + { + const string endpointVarName = "DAPR_HTTP_ENDPOINT"; + const string portVarName = "DAPR_HTTP_PORT"; + var original_HttpEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_HttpPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(configuration); + Assert.Equal("http://localhost:3500/", httpEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_HttpEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_HttpPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_GRPC_ENDPOINT"; + const string portVarName = "test_DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "2570"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingConfiguration() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io", EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(portVarName, null, EnvironmentVariableTarget.Process); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingPrefixedConfiguration() + { + const string endpointVarName = "test_DAPR_GRPC_ENDPOINT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + + try + { + const string prefix = "test_"; + + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingConfiguration() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointAndPortUsingEnvVarValues() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, "4744"); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointUsingEnvVarValues() + { + const string endpointVarName = "DAPR_GRPC_ENDPOINT"; + const string portVarName = "DAPR_GRPC_PORT"; + var original_GrpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_GrpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, "https://grpc.dapr.io"); + Environment.SetEnvironmentVariable(portVarName, null); //Will use 443 from the endpoint value instead + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal("https://grpc.dapr.io:443/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_GrpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_GrpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointDefaultToLocalhostWithPort() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_grpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_grpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, "7256"); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal($"{DaprDefaults.DefaultDaprScheme}://{DaprDefaults.DefaultDaprHost}:7256/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_grpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_grpcPort); + } + } + + [Fact] + public void ShouldBuildGrpcEndpointDefaultToLocalhostWithDefaultPort() + { + const string endpointVarName = DaprDefaults.DaprGrpcEndpointName; + const string portVarName = DaprDefaults.DaprGrpcPortName; + var original_grpcEndpoint = Environment.GetEnvironmentVariable(endpointVarName); + var original_grpcPort = Environment.GetEnvironmentVariable(portVarName); + + try + { + Environment.SetEnvironmentVariable(endpointVarName, null); + Environment.SetEnvironmentVariable(portVarName, null); + + var configurationBuilder = new ConfigurationBuilder(); + var configuration = configurationBuilder.Build(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); + Assert.Equal($"{DaprDefaults.DefaultDaprScheme}://{DaprDefaults.DefaultDaprHost}:{DaprDefaults.DefaultGrpcPort}/", grpcEndpoint); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(endpointVarName, original_grpcEndpoint); + Environment.SetEnvironmentVariable(portVarName, original_grpcPort); + } + } + + [Fact] + public void ShouldBuildApiTokenUsingConfiguration() + { + const string envVarName = DaprDefaults.DaprApiTokenName; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + + try + { + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } + + [Fact] + public void ShouldBuildApiTokenUsingPrefixedConfiguration() + { + + const string envVarName = $"test_{DaprDefaults.DaprApiTokenName}"; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + + try + { + const string prefix = "test_"; + + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(prefix); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } + + [Fact] + public void ShouldBuildApiTokenWithEnvVarWhenConfigurationNotAvailable() + { + const string envVarName = DaprDefaults.DaprApiTokenName; + var original_ApiToken = Environment.GetEnvironmentVariable(envVarName); + const string apiToken = "abc123"; + Environment.SetEnvironmentVariable(envVarName, apiToken); + + try + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddEnvironmentVariables(); + var configuration = configurationBuilder.Build(); + + var testApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + Assert.Equal(apiToken, testApiToken); + } + finally + { + //Restore + Environment.SetEnvironmentVariable(envVarName, original_ApiToken); + } + } +} From d4f2ff152354c09bdde32b7acb82284fbe2d5f86 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:27:26 +0200 Subject: [PATCH 23/69] cleanup: Removed Serilog nuget from Directory.Packages.props (#1376) * Removed Serilog nuget from Directory.Packages.props Signed-off-by: Manuel Menegazzo * Update Directory.Packages.props Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> --------- Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 20 +++++++++---------- .../Dapr.E2E.Test.App.csproj | 3 +-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 842c82aa8..3c1459b5d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,11 +3,10 @@ true true - - + @@ -16,10 +15,10 @@ - + - + @@ -30,20 +29,19 @@ - + - + - + - - + - + - \ No newline at end of file + 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 3454ac25d..162a1306a 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -8,9 +8,8 @@ - - + From 01eddad9d4d3b159017ee782e657a01367ef40cf Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:33:13 +0200 Subject: [PATCH 24/69] Removed sample folder (#1375) Signed-off-by: Manuel Menegazzo Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 +---- samples/Client/README.md | 56 ---------------------------------------- 2 files changed, 1 insertion(+), 62 deletions(-) delete mode 100644 samples/Client/README.md diff --git a/all.sln b/all.sln index 85ed848a4..1dd0ab3c5 100644 --- a/all.sln +++ b/all.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 +# 17 VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors", "src\Dapr.Actors\Dapr.Actors.csproj", "{C2DB4B64-B7C3-4FED-8753-C040F677C69A}" @@ -14,11 +14,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Client", "src\Dapr.Cli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.AspNetCore", "src\Dapr.AspNetCore\Dapr.AspNetCore.csproj", "{08D602F6-7C11-4653-B70B-B56333BF6FD2}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B2DB41EE-45F5-447B-95E8-38E1E8B70C4E}" - ProjectSection(SolutionItems) = preProject - samples\.editorconfig = samples\.editorconfig - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DD020B34-460F-455F-8D17-CF4A949F100B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Client.Test", "test\Dapr.Client.Test\Dapr.Client.Test.csproj", "{383609C1-F43F-49EB-85E4-1964EE7F0F14}" diff --git a/samples/Client/README.md b/samples/Client/README.md deleted file mode 100644 index 2bb738d89..000000000 --- a/samples/Client/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Dapr Client examples - -The following examples will show you how to: - -- Invoke services -- Publish events -- Use the state store to get, set, and delete data - -## Prerequisites - -* [.NET 6+](https://dotnet.microsoft.com/download) -* [Dapr CLI](https://github.com/dapr/cli) -* [Dapr DotNet SDK](https://github.com/dapr/dotnet-sdk) - -## Running the Sample - -To run the sample locally run this command in DaprClient directory: -```sh -dapr run --app-id DaprClient -- dotnet run -``` - -Running the following command will output a list of the samples included. -```sh -dapr run --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 0th sample from the list produced earlier. -```sh -dapr run --app-id DaprClient -- dotnet run 0 -``` - -Samples that use HTTP-based service invocation will require running the [RoutingService](../../AspNetCore/RoutingSample). - -Samples that use gRPC-based service invocation will require running [GrpcService](../../AspNetCore/GrpcServiceSample). - -## Invoking Services - -See: `InvokeServiceHttpClientExample.cs` for an example of using `HttpClient` to invoke another service through Dapr. - -See: `InvokeServiceHttpExample.cs` for an example using the `DaprClient` to invoke another service through Dapr. - -See: `InvokeServiceGrpcExample.cs` for an example using the `DaprClient` to invoke a service using gRPC through Dapr. - -## Publishing Pub/Sub Events - -See: `PublishEventExample.cs` for an example using the `DaprClient` to publish a pub/sub event. - -## Working with the State Store - -See: `StateStoreExample.cs` for an example of using `DaprClient` for basic state store operations like get, set, and delete. - -See: `StateStoreETagsExample.cs` for an example of using `DaprClient` for optimistic concurrency control with the state store. - -See: `StateStoreTransactionsExample.cs` for an example of using `DaprClient` for transactional state store operations that affect multiple keys. From 0b4eedac7c985f2a70d96ff7f170669f1acd4286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A2mara?= <52082556+RafaelJCamara@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:27:39 +0200 Subject: [PATCH 25/69] Remove unused variables (#1314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove unused variables Signed-off-by: Rafael Camara Signed-off-by: Rafael Câmara <52082556+RafaelJCamara@users.noreply.github.com> Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- examples/Actor/ActorClient/Program.cs | 2 +- examples/AspNetCore/RoutingSample/Startup.cs | 1 - .../GeneratedActor/ActorClient/Program.cs | 2 +- .../WorkflowUnitTest/OrderProcessingTests.cs | 2 +- .../ActorClientGenerator.cs | 1 - src/Dapr.Actors/Runtime/ActorManager.cs | 3 +- .../HostingTests.cs | 2 +- .../Dapr.Actors.Test/ActorCodeBuilderTests.cs | 2 +- test/Dapr.Actors.Test/ActorIdTests.cs | 5 +- .../Runtime/ActorManagerTests.cs | 2 +- .../Runtime/ActorRuntimeOptionsTests.cs | 2 - .../Serialization/ActorIdJsonConverterTest.cs | 15 ++- .../ControllerIntegrationTest.cs | 3 - .../DaprClientBuilderTest.cs | 6 +- test/Dapr.Client.Test/DaprApiTokenTest.cs | 4 +- .../DaprClientTest.InvokeMethodGrpcAsync.cs | 11 +- .../DistributedLockApiTest.cs | 1 - .../InvocationHandlerTests.cs | 6 +- test/Dapr.Client.Test/InvokeBindingApiTest.cs | 2 +- test/Dapr.Client.Test/PublishEventApiTest.cs | 2 - test/Dapr.Client.Test/StateApiTest.cs | 23 ++-- test/Dapr.Client.Test/TryLockResponseTest.cs | 2 +- .../Clients/GeneratedClientTests.cs | 2 +- test/Dapr.E2E.Test.App/Startup.cs | 2 +- .../Actors/E2ETests.ExceptionTests.cs | 3 +- ...aprSecretStoreConfigurationProviderTest.cs | 102 +++++++++--------- 26 files changed, 94 insertions(+), 114 deletions(-) diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index f6ca26f53..950869b2b 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -85,7 +85,7 @@ public static async Task Main(string[] args) var nonRemotingProxy = ActorProxy.Create(actorId, "DemoActor"); await nonRemotingProxy.InvokeMethodAsync("TestNoArgumentNoReturnType"); await nonRemotingProxy.InvokeMethodAsync("SaveData", data); - var res = await nonRemotingProxy.InvokeMethodAsync("GetData"); + await nonRemotingProxy.InvokeMethodAsync("GetData"); Console.WriteLine("Registering the timer and reminder"); await proxy.RegisterTimer(); diff --git a/examples/AspNetCore/RoutingSample/Startup.cs b/examples/AspNetCore/RoutingSample/Startup.cs index 29bb488fe..3d71e9e1f 100644 --- a/examples/AspNetCore/RoutingSample/Startup.cs +++ b/examples/AspNetCore/RoutingSample/Startup.cs @@ -221,7 +221,6 @@ await JsonSerializer.SerializeAsync(context.Response.Body, async Task ViewErrorMessage(HttpContext context) { - var client = context.RequestServices.GetRequiredService(); var transaction = await JsonSerializer.DeserializeAsync(context.Request.Body, serializerOptions); logger.LogInformation("The amount cannot be negative: {0}", transaction.Amount); diff --git a/examples/GeneratedActor/ActorClient/Program.cs b/examples/GeneratedActor/ActorClient/Program.cs index 87f714907..0b858214a 100644 --- a/examples/GeneratedActor/ActorClient/Program.cs +++ b/examples/GeneratedActor/ActorClient/Program.cs @@ -23,7 +23,7 @@ var client = new ClientActorClient(proxy); -var state = await client.GetStateAsync(cancellationTokenSource.Token); +await client.GetStateAsync(cancellationTokenSource.Token); await client.SetStateAsync(new ClientState("Hello, World!"), cancellationTokenSource.Token); diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index e38a0c940..3093e5c7a 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -64,7 +64,7 @@ public async Task TestInsufficientInventory() .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly - OrderResult result = await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); + await new OrderProcessingWorkflow().RunAsync(mockContext.Object, order); // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index 349d80188..f95fc4224 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -112,7 +112,6 @@ public void Execute(GeneratorExecutionContext context) { try { - var actorInterfaceTypeName = interfaceSymbol.Name; var fullyQualifiedActorInterfaceTypeName = interfaceSymbol.ToString(); var attributeData = interfaceSymbol.GetAttributes().Single(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index 80049d65f..a641440cf 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -229,7 +229,6 @@ internal async Task FireTimerAsync(ActorId actorId, Stream requestBodyStream, Ca // Create a Func to be invoked by common method. async Task RequestFunc(Actor actor, CancellationToken ct) { - var actorTypeName = actor.Host.ActorTypeInfo.ActorTypeName; var actorType = actor.Host.ActorTypeInfo.ImplementationType; var methodInfo = actor.GetMethodInfoUsingReflection(actorType, timerData.Callback); @@ -241,7 +240,7 @@ async Task RequestFunc(Actor actor, CancellationToken ct) return default; } - var result = await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); + await this.DispatchInternalAsync(actorId, this.timerMethodContext, RequestFunc, cancellationToken); } internal async Task ActivateActorAsync(ActorId actorId) diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs index 7f96c66e6..bf3757ce1 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/HostingTests.cs @@ -42,7 +42,7 @@ public void MapActorsHandlers_WithoutAddActors_Throws() // NOTE: in 3.1 TestServer.CreateClient triggers the failure, in 5.0 it's Host.Start using var host = CreateHost(); var server = host.GetTestServer(); - var client = server.CreateClient(); + server.CreateClient(); }); Assert.Equal( diff --git a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs index 6bb3c827d..1b4a3fb26 100644 --- a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs +++ b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs @@ -32,7 +32,7 @@ public class ActorCodeBuilderTests [Fact] public void TestBuildActorProxyGenerator() { - ActorProxyGenerator proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(typeof(ITestActor)); + ActorCodeBuilder.GetOrCreateProxyGenerator(typeof(ITestActor)); } [Fact] diff --git a/test/Dapr.Actors.Test/ActorIdTests.cs b/test/Dapr.Actors.Test/ActorIdTests.cs index 668bd0fbf..c54f4c351 100644 --- a/test/Dapr.Actors.Test/ActorIdTests.cs +++ b/test/Dapr.Actors.Test/ActorIdTests.cs @@ -115,10 +115,7 @@ public class ActorIdTests [InlineData(" ")] public void Initialize_New_ActorId_Object_With_Null_Or_Whitespace_Id(string id) { - Assert.Throws(() => - { - ActorId actorId = new ActorId(id); - }); + Assert.Throws(() => new ActorId(id)); } [Theory] diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index 6b92c7e18..b27e9afe3 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -164,7 +164,7 @@ public async Task DeactivateActorAsync_ExceptionDuringDeactivation_ActorIsRemove var id = ActorId.CreateRandom(); await manager.ActivateActorAsync(id); - Assert.True(manager.TryGetActorAsync(id, out var actor)); + Assert.True(manager.TryGetActorAsync(id, out _)); await Assert.ThrowsAsync(async () => { diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs index 4fe991970..b68eda5ce 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeOptionsTests.cs @@ -26,8 +26,6 @@ public void TestRegisterActor_SavesActivator() { var actorType = typeof(TestActor); var actorTypeInformation = ActorTypeInformation.Get(actorType, actorTypeName: null); - var host = ActorHost.CreateForTest(); - var actor = new TestActor(host); var activator = Mock.Of(); diff --git a/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs index fe33eca54..9c4f22193 100644 --- a/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs +++ b/test/Dapr.Actors.Test/Serialization/ActorIdJsonConverterTest.cs @@ -50,9 +50,9 @@ public void CanDeserializeActorId() { var id = ActorId.CreateRandom().GetId(); var document = $@" -{{ - ""actor"": ""{id}"" -}}"; + {{ + ""actor"": ""{id}"" + }}"; var deserialized = JsonSerializer.Deserialize(document); @@ -62,11 +62,10 @@ public void CanDeserializeActorId() [Fact] public void CanDeserializeNullActorId() { - var id = ActorId.CreateRandom().GetId(); - var document = $@" -{{ - ""actor"": null -}}"; + const string document = @" + { + ""actor"": null + }"; var deserialized = JsonSerializer.Deserialize(document); diff --git a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs index 364cd8448..7735ec4fb 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/ControllerIntegrationTest.cs @@ -67,7 +67,6 @@ public async Task ModelBinder_GetFromStateEntryWithKeyNotInStateStore_ReturnsNul using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithoutstateentry/test"); var response = await httpClient.SendAsync(request); @@ -142,7 +141,6 @@ public async Task ModelBinder_GetFromStateEntryWithStateEntry_WithKeyNotInStateS using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/controllerwithstateentry/test"); var response = await httpClient.SendAsync(request); @@ -159,7 +157,6 @@ public async Task ModelBinder_CanGetOutOfTheWayWhenTheresNoBinding() using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - var daprClient = factory.DaprClient; var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/echo-user?name=jimmy"); var response = await httpClient.SendAsync(request); diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 52d0b7000..bd807ebae 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -11,7 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; +using System; using System.Text.Json; using Dapr.Client; using Grpc.Core; @@ -44,7 +44,7 @@ public void DaprClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() { var builder = new DaprClientBuilder(); - var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); } @@ -52,7 +52,7 @@ public void DaprClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault public void DaprClientBuilder_DoesNotOverrideUserGrpcChannelOptions() { var builder = new DaprClientBuilder(); - var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); } diff --git a/test/Dapr.Client.Test/DaprApiTokenTest.cs b/test/Dapr.Client.Test/DaprApiTokenTest.cs index 3726e0321..cf9f422bc 100644 --- a/test/Dapr.Client.Test/DaprApiTokenTest.cs +++ b/test/Dapr.Client.Test/DaprApiTokenTest.cs @@ -35,7 +35,7 @@ public async Task DaprCall_WithApiTokenSet() request.Dismiss(); // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); headerValues.Count().Should().Be(1); @@ -56,7 +56,7 @@ public async Task DaprCall_WithoutApiToken() request.Dismiss(); // Get Request and validate - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); request.Request.Headers.TryGetValues("dapr-api-token", out var headerValues); headerValues.Should().BeNull(); diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 65b9b1e7d..6ccbbe4c4 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -88,8 +88,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeAndData_Thr Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -153,8 +152,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithReturnTypeNoData_Thro Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -210,8 +208,7 @@ public async Task InvokeMethodGrpcAsync_CanInvokeMethodWithNoReturnTypeAndData_T Data = Any.Pack(data), }; - var response = - client.Call() + await client.Call() .SetResponse(invokeResponse) .Build(); @@ -294,7 +291,7 @@ public async Task InvokeMethodGrpcAsync_WithReturnTypeAndData() // Validate Response var invokedResponse = await request.CompleteWithMessageAsync(response); - invokeResponse.Name.Should().Be(invokeResponse.Name); + invokedResponse.Name.Should().Be(invokeResponse.Name); } [Fact] diff --git a/test/Dapr.Client.Test/DistributedLockApiTest.cs b/test/Dapr.Client.Test/DistributedLockApiTest.cs index def7d8f8f..a030586c5 100644 --- a/test/Dapr.Client.Test/DistributedLockApiTest.cs +++ b/test/Dapr.Client.Test/DistributedLockApiTest.cs @@ -60,7 +60,6 @@ public async Task TryLockAsync_WithAllValues_ArgumentVerifierException() string lockOwner = "owner1"; Int32 expiryInSeconds = 0; // Get response and validate - var invokeResponse = new ArgumentException(); await Assert.ThrowsAsync(async () => await client.Lock(storeName, resourceId, lockOwner, expiryInSeconds)); } diff --git a/test/Dapr.Client.Test/InvocationHandlerTests.cs b/test/Dapr.Client.Test/InvocationHandlerTests.cs index 3dac84113..b171ca399 100644 --- a/test/Dapr.Client.Test/InvocationHandlerTests.cs +++ b/test/Dapr.Client.Test/InvocationHandlerTests.cs @@ -156,7 +156,7 @@ public async Task SendAsync_RewritesUri() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Null(capture.DaprApiToken); @@ -181,7 +181,7 @@ public async Task SendAsync_RewritesUri_AndAppId() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/Bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Null(capture.DaprApiToken); @@ -205,7 +205,7 @@ public async Task SendAsync_RewritesUri_AndAddsApiToken() }; var request = new HttpRequestMessage(HttpMethod.Post, uri); - var response = await CallSendAsync(handler, request); + await CallSendAsync(handler, request); Assert.Equal("https://localhost:5000/v1.0/invoke/bank/method/accounts/17?", capture.RequestUri?.OriginalString); Assert.Equal("super-duper-secure", capture.DaprApiToken); diff --git a/test/Dapr.Client.Test/InvokeBindingApiTest.cs b/test/Dapr.Client.Test/InvokeBindingApiTest.cs index 30c9d48ba..c2ada7a11 100644 --- a/test/Dapr.Client.Test/InvokeBindingApiTest.cs +++ b/test/Dapr.Client.Test/InvokeBindingApiTest.cs @@ -209,7 +209,7 @@ public async Task InvokeBindingAsync_WrapsJsonException() return await daprClient.InvokeBindingAsync("test", "test", new InvokeRequest() { RequestParameter = "Hello " }); }); - var envelope = await request.GetRequestEnvelopeAsync(); + await request.GetRequestEnvelopeAsync(); var ex = await Assert.ThrowsAsync(async () => { await request.CompleteWithMessageAsync(response); diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index 77d6ee905..e3e21dd2b 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -142,7 +142,6 @@ public async Task PublishEventAsync_CanPublishTopicWithNoContent() request.Dismiss(); var envelope = await request.GetRequestEnvelopeAsync(); - var jsonFromRequest = envelope.Data.ToStringUtf8(); envelope.PubsubName.Should().Be(TestPubsubName); envelope.Topic.Should().Be("test"); @@ -214,7 +213,6 @@ public async Task PublishEventAsync_CanPublishCloudEventWithNoContent() { await using var client = TestClient.CreateForDaprClient(); - var publishData = new PublishData() { PublishObjectParameter = "testparam" }; var cloudEvent = new CloudEvent { Source = new Uri("urn:testsource"), diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 0684a8db0..b240605a1 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -839,8 +839,7 @@ public async Task TrySaveStateAsync_ValidateNonETagErrorThrowsException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi().Build(); var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); @@ -861,8 +860,8 @@ public async Task TrySaveStateAsync_ValidateETagRelatedExceptionReturnsFalse() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed saving state in state store testStore")); // Setup the mock client to throw an Rpc Exception with the expected details info @@ -879,8 +878,8 @@ public async Task TrySaveStateAsync_NullEtagThrowsArgumentException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); await FluentActions.Awaiting(async () => await client.DaprClient.TrySaveStateAsync("test", "test", "testValue", null)) .Should().ThrowAsync(); @@ -907,8 +906,8 @@ public async Task TryDeleteStateAsync_ValidateNonETagErrorThrowsException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); @@ -929,8 +928,8 @@ public async Task TryDeleteStateAsync_NullEtagThrowsArgumentException() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); await FluentActions.Awaiting(async () => await client.DaprClient.TryDeleteStateAsync("test", "test", null)) .Should().ThrowAsync(); @@ -957,8 +956,8 @@ public async Task TryDeleteStateAsync_ValidateETagRelatedExceptionReturnsFalse() { var client = new MockClient(); - var response = client.CallStateApi() - .Build(); + await client.CallStateApi() + .Build(); var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed deleting state with key test")); // Setup the mock client to throw an Rpc Exception with the expected details info diff --git a/test/Dapr.Client.Test/TryLockResponseTest.cs b/test/Dapr.Client.Test/TryLockResponseTest.cs index f94f949db..3420dc233 100644 --- a/test/Dapr.Client.Test/TryLockResponseTest.cs +++ b/test/Dapr.Client.Test/TryLockResponseTest.cs @@ -47,7 +47,7 @@ public async Task TryLockAsync_WithAllValues_ValidateRequest() Success = true }; - var domainResponse = await request.CompleteWithMessageAsync(invokeResponse); + await request.CompleteWithMessageAsync(invokeResponse); //testing unlocking diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs index 6079b5df7..a089a2e82 100644 --- a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs @@ -100,7 +100,7 @@ public async Task TestGeneratedClientAsync() var client = new ClientActorClient(actorProxy); - var result = await client.GetStateAsync(cancellationTokenSource.Token); + await client.GetStateAsync(cancellationTokenSource.Token); await client.SetStateAsync(new ClientState("updated state"), cancellationTokenSource.Token); } diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index bfca60f91..7e7484d54 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -120,7 +120,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); - var loggerFactory = LoggerFactory.Create(builder => + LoggerFactory.Create(builder => { builder.AddSerilog(logger); }); diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs index 986a2c4f0..fc036d15d 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ExceptionTests.cs @@ -25,8 +25,7 @@ public partial class E2ETests : IAsyncLifetime public async Task ActorCanProvideExceptionDetails() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - var actorIds = new ActorId(Guid.NewGuid().ToString()); - + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ExceptionActor"); await WaitForActorRuntimeAsync(proxy, cts.Token); ActorMethodInvocationException ex = await Assert.ThrowsAsync(async () => await proxy.ExceptionExample()); diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index 74b66c3cb..fd323f989 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -41,11 +41,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(null, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("store", ex.Message); } @@ -58,11 +58,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithEmptyStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(string.Empty, new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("The value cannot be null or empty", ex.Message); } @@ -75,11 +75,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_Reports .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", (DaprSecretDescriptor[])null, daprClient) + .Build(); + }); Assert.Contains("secretDescriptors", ex.Message); } @@ -88,11 +88,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithoutSecretDescriptors_Reports public void AddDaprSecretStore_UsingDescriptors_WithoutClient_ReportsError() { var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, null) + .Build(); + }); Assert.Contains("client", ex.Message); } @@ -105,11 +105,11 @@ public void AddDaprSecretStore_UsingDescriptors_WithZeroSecretDescriptors_Report .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { }, daprClient) + .Build(); + }); Assert.Contains("No secret descriptor was provided", ex.Message); } @@ -132,11 +132,11 @@ public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .Build(); + }); Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); } @@ -310,7 +310,7 @@ public void LoadSecrets_FromSecretStoreRequiredAndDoesNotExist_ShouldThrowExcept var ex = Assert.Throws(() => { - var config = CreateBuilder() + CreateBuilder() .AddDaprSecretStore(storeName, secretDescriptors, daprClient) .Build(); }); @@ -327,11 +327,11 @@ public void AddDaprSecretStore_WithoutStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(null, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(null, daprClient) + .Build(); + }); Assert.Contains("store", ex.Message); } @@ -344,11 +344,11 @@ public void AddDaprSecretStore_WithEmptyStore_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore(string.Empty, daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore(string.Empty, daprClient) + .Build(); + }); Assert.Contains("The value cannot be null or empty", ex.Message); } @@ -357,11 +357,11 @@ public void AddDaprSecretStore_WithEmptyStore_ReportsError() public void AddDaprSecretStore_WithoutClient_ReportsError() { var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", null) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", null) + .Build(); + }); Assert.Contains("client", ex.Message); } @@ -384,11 +384,11 @@ public void AddDaprSecretStore_DuplicateSecret_ReportsError() .Build(); var ex = Assert.Throws(() => - { - var config = CreateBuilder() - .AddDaprSecretStore("store", daprClient) - .Build(); - }); + { + CreateBuilder() + .AddDaprSecretStore("store", daprClient) + .Build(); + }); Assert.Contains("Please remove any duplicates from your secret store.", ex.Message); } From ed8640404ec26bd5b5fb5197935c0c2c2a3fce20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A2mara?= <52082556+RafaelJCamara@users.noreply.github.com> Date: Fri, 25 Oct 2024 00:00:09 +0200 Subject: [PATCH 26/69] Remove unused using statements. (#1313) Signed-off-by: Rafael Camara Signed-off-by: Siri Varma Vegiraju --- .../AspNetCore/GrpcServiceSample/Program.cs | 5 --- .../Program.cs | 1 - .../Controllers/ConfigurationController.cs | 4 +-- examples/Client/ConfigurationApi/Program.cs | 1 - examples/Client/ConfigurationApi/Startup.cs | 5 +-- .../Controllers/BindingController.cs | 5 ++- .../Client/DistributedLock/Model/StateData.cs | 3 +- examples/Client/DistributedLock/Program.cs | 5 --- .../InvokeServiceHttpClientExample.cs | 1 - .../Activities/ProcessPaymentActivity.cs | 3 +- .../Communication/ActorInvokeException.cs | 5 --- .../Communication/ActorInvokeExceptionData.cs | 3 -- ...torMessageBodyJsonSerializationProvider.cs | 1 - src/Dapr.Actors/Runtime/ActorStateManager.cs | 1 - .../Runtime/DefaultActorActivatorFactory.cs | 3 -- .../Runtime/DefaultActorTimerManager.cs | 1 - src/Dapr.Actors/Runtime/IActorStateManager.cs | 1 - .../DaprAuthenticationBuilderExtensions.cs | 1 - .../DaprMvcBuilderExtensions.cs | 2 -- src/Dapr.AspNetCore/FromStateAttribute.cs | 1 - src/Dapr.Client/InvocationInterceptor.cs | 2 -- src/Dapr.Client/StartWorkflowResponse.cs | 3 -- src/Dapr.Client/StateOptions.cs | 4 --- src/Dapr.Client/StateTransactionRequest.cs | 2 -- .../SubscribeConfigurationResponse.cs | 3 +- src/Dapr.Client/UnlockResponse.cs | 2 -- .../UnsubscribeConfigurationResponse.cs | 3 +- .../DaprConfigurationStoreSource.cs | 1 - .../DaprSecretStoreConfigurationExtensions.cs | 1 - .../DaprSecretStoreConfigurationSource.cs | 1 - src/Dapr.Workflow/WorkflowLoggingService.cs | 1 - .../AppWebApplicationFactory.cs | 1 - .../DependencyInjectionActorActivatorTests.cs | 2 -- .../Dapr.Actors.Test/ActorCodeBuilderTests.cs | 1 - .../ActorMethodInvocationExceptionTests.cs | 1 - .../ActorProxyOptionsTests.cs | 4 --- .../Dapr.Actors.Test/ActorStateManagerTest.cs | 7 ----- test/Dapr.Actors.Test/ActorUnitTestTests.cs | 2 -- .../DaprFormatTimeSpanTests.cs | 2 -- .../DaprHttpInteractorTest.cs | 2 -- .../Dapr.Actors.Test/DaprStateProviderTest.cs | 7 ----- .../Runtime/ActorRuntimeTests.cs | 1 - .../Runtime/DefaultActorActivatorTests.cs | 2 -- .../ActorIdDataContractSerializationTest.cs | 4 --- .../TimerInfoJsonConverterTest.cs | 1 - .../StateTestClient.cs | 2 -- .../CloudEventsMiddlewareTest.cs | 1 - .../DaprClientBuilderTest.cs | 1 - .../StateEntryApplicationModelProviderTest.cs | 8 ----- test/Dapr.Client.Test/ConfigurationApiTest.cs | 1 - .../DaprClientTest.InvokeMethodAsync.cs | 3 +- test/Dapr.Client.Test/PublishEventApiTest.cs | 1 - test/Dapr.Client.Test/StateApiTest.cs | 2 -- test/Dapr.Client.Test/TypeConvertersTest.cs | 1 - test/Dapr.E2E.Test.App.Grpc/Startup.cs | 31 ++++++++----------- test/Dapr.E2E.Test.App/Actors/StateActor.cs | 1 - .../Controllers/TestController.cs | 2 -- test/Dapr.E2E.Test.App/Startup.cs | 2 -- .../Actors/E2ETests.StateTests.cs | 1 - .../E2ETests.ServiceInvocationTests.cs | 4 --- test/Shared/GrpcUtils.cs | 1 - 61 files changed, 22 insertions(+), 152 deletions(-) diff --git a/examples/AspNetCore/GrpcServiceSample/Program.cs b/examples/AspNetCore/GrpcServiceSample/Program.cs index cd8486447..dbec5ba2c 100644 --- a/examples/AspNetCore/GrpcServiceSample/Program.cs +++ b/examples/AspNetCore/GrpcServiceSample/Program.cs @@ -11,11 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Hosting; diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs index 7afb17436..658d6d163 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/Program.cs @@ -18,7 +18,6 @@ using Dapr.Client; using Dapr.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; - using System.Collections.Generic; using System; /// diff --git a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs index 55bf6df53..9ceb60c0b 100644 --- a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs +++ b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading; +using System.Collections.Generic; using System.Threading.Tasks; using ControllerSample; using Dapr; diff --git a/examples/Client/ConfigurationApi/Program.cs b/examples/Client/ConfigurationApi/Program.cs index f5218602d..22f12cfae 100644 --- a/examples/Client/ConfigurationApi/Program.cs +++ b/examples/Client/ConfigurationApi/Program.cs @@ -4,7 +4,6 @@ using Dapr.Client; using Dapr.Extensions.Configuration; using System.Collections.Generic; -using System.Threading; namespace ConfigurationApi { diff --git a/examples/Client/ConfigurationApi/Startup.cs b/examples/Client/ConfigurationApi/Startup.cs index db5b921c9..b858b810c 100644 --- a/examples/Client/ConfigurationApi/Startup.cs +++ b/examples/Client/ConfigurationApi/Startup.cs @@ -1,11 +1,8 @@ -using System; -using Dapr.AspNetCore; -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Primitives; namespace ConfigurationApi { diff --git a/examples/Client/DistributedLock/Controllers/BindingController.cs b/examples/Client/DistributedLock/Controllers/BindingController.cs index 1edc6aafe..aa4dd1f52 100644 --- a/examples/Client/DistributedLock/Controllers/BindingController.cs +++ b/examples/Client/DistributedLock/Controllers/BindingController.cs @@ -1,6 +1,5 @@ -using System; -using System.Linq; -using System.Text; +using System; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Dapr; diff --git a/examples/Client/DistributedLock/Model/StateData.cs b/examples/Client/DistributedLock/Model/StateData.cs index d5c5f6e9a..0ad5d2fd9 100644 --- a/examples/Client/DistributedLock/Model/StateData.cs +++ b/examples/Client/DistributedLock/Model/StateData.cs @@ -1,5 +1,4 @@ -using System; -namespace DistributedLock.Model +namespace DistributedLock.Model { #nullable enable public class StateData diff --git a/examples/Client/DistributedLock/Program.cs b/examples/Client/DistributedLock/Program.cs index ba4d670a7..3080b5b1e 100644 --- a/examples/Client/DistributedLock/Program.cs +++ b/examples/Client/DistributedLock/Program.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace DistributedLock { diff --git a/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs b/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs index fc1475b8e..72d68096d 100644 --- a/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs +++ b/examples/Client/ServiceInvocation/InvokeServiceHttpClientExample.cs @@ -13,7 +13,6 @@ using System; using System.Net.Http.Json; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Client; diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index 1ddb51bbf..4ca39f0ae 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,5 +1,4 @@ -using Dapr.Client; -using Dapr.Workflow; +using Dapr.Workflow; using Microsoft.Extensions.Logging; namespace WorkflowConsoleApp.Activities diff --git a/src/Dapr.Actors/Communication/ActorInvokeException.cs b/src/Dapr.Actors/Communication/ActorInvokeException.cs index ac4ea63df..d58ceace8 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeException.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeException.cs @@ -14,12 +14,7 @@ namespace Dapr.Actors { using System; - using System.Globalization; using System.IO; - using System.Runtime.Serialization; - using System.Text; - using System.Xml; - using Dapr.Actors.Resources; using Microsoft.Extensions.Logging; /// diff --git a/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs b/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs index b9f1f4d86..6103c79c4 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeExceptionData.cs @@ -13,12 +13,9 @@ namespace Dapr.Actors { - using System; using System.IO; using System.Runtime.Serialization; using System.Xml; - using Dapr.Actors; - using Microsoft.Extensions.Logging; [DataContract(Name = "ActorInvokeExceptionData", Namespace = Constants.Namespace)] internal class ActorInvokeExceptionData diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs index fd35db8e1..062d3c742 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs @@ -18,7 +18,6 @@ namespace Dapr.Actors.Communication using System.IO; using System.Text.Json; using System.Threading.Tasks; - using System.Xml; /// /// This is the implmentation for used by remoting service and client during diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 111bb80f4..31ada4433 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Threading; diff --git a/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs b/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs index c927ebe61..43839c64d 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorActivatorFactory.cs @@ -11,9 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Threading.Tasks; - namespace Dapr.Actors.Runtime { /// diff --git a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs index b42b432a1..66f94f08e 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs @@ -15,7 +15,6 @@ using System.Text.Json; using System.Threading.Tasks; using System.IO; -using System.Text; namespace Dapr.Actors.Runtime { diff --git a/src/Dapr.Actors/Runtime/IActorStateManager.cs b/src/Dapr.Actors/Runtime/IActorStateManager.cs index b85fa2a06..8795e28e5 100644 --- a/src/Dapr.Actors/Runtime/IActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/IActorStateManager.cs @@ -17,7 +17,6 @@ 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 . diff --git a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs index 0c37dbc8f..e55b2916e 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using Dapr; using Dapr.AspNetCore; namespace Microsoft.AspNetCore.Authentication diff --git a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs index 6195b9c30..6209fea5a 100644 --- a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs @@ -15,8 +15,6 @@ namespace Microsoft.Extensions.DependencyInjection { using System; using System.Linq; - using System.Text.Json; - using Dapr; using Dapr.AspNetCore; using Dapr.Client; using Microsoft.AspNetCore.Mvc; diff --git a/src/Dapr.AspNetCore/FromStateAttribute.cs b/src/Dapr.AspNetCore/FromStateAttribute.cs index 164d7ef09..16299ba07 100644 --- a/src/Dapr.AspNetCore/FromStateAttribute.cs +++ b/src/Dapr.AspNetCore/FromStateAttribute.cs @@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Mvc { using System; - using System.ComponentModel; using Dapr; using Microsoft.AspNetCore.Mvc.ModelBinding; diff --git a/src/Dapr.Client/InvocationInterceptor.cs b/src/Dapr.Client/InvocationInterceptor.cs index bf9c8f482..b4ecc4b51 100644 --- a/src/Dapr.Client/InvocationInterceptor.cs +++ b/src/Dapr.Client/InvocationInterceptor.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Threading.Tasks; using Grpc.Core; using Grpc.Core.Interceptors; diff --git a/src/Dapr.Client/StartWorkflowResponse.cs b/src/Dapr.Client/StartWorkflowResponse.cs index 8442fd40e..a927116cb 100644 --- a/src/Dapr.Client/StartWorkflowResponse.cs +++ b/src/Dapr.Client/StartWorkflowResponse.cs @@ -11,9 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; -using System.Collections.Generic; - namespace Dapr.Client { /// diff --git a/src/Dapr.Client/StateOptions.cs b/src/Dapr.Client/StateOptions.cs index 04c878fc0..fa6f2739c 100644 --- a/src/Dapr.Client/StateOptions.cs +++ b/src/Dapr.Client/StateOptions.cs @@ -13,10 +13,6 @@ namespace Dapr.Client { - using System; - using System.Collections.Generic; - using System.Text; - /// /// Options when perfroming state operations with Dapr. /// diff --git a/src/Dapr.Client/StateTransactionRequest.cs b/src/Dapr.Client/StateTransactionRequest.cs index 1e47d9344..756f3069f 100644 --- a/src/Dapr.Client/StateTransactionRequest.cs +++ b/src/Dapr.Client/StateTransactionRequest.cs @@ -14,8 +14,6 @@ namespace Dapr.Client { using System.Collections.Generic; - using System.Threading; - using Dapr.Client; /// /// Represents a single request in in a StateTransaction. diff --git a/src/Dapr.Client/SubscribeConfigurationResponse.cs b/src/Dapr.Client/SubscribeConfigurationResponse.cs index b371bd21c..bea992890 100644 --- a/src/Dapr.Client/SubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/SubscribeConfigurationResponse.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Dapr.Client { diff --git a/src/Dapr.Client/UnlockResponse.cs b/src/Dapr.Client/UnlockResponse.cs index 62480c8f6..68cc40670 100644 --- a/src/Dapr.Client/UnlockResponse.cs +++ b/src/Dapr.Client/UnlockResponse.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Collections.Generic; - namespace Dapr.Client { /// diff --git a/src/Dapr.Client/UnsubscribeConfigurationResponse.cs b/src/Dapr.Client/UnsubscribeConfigurationResponse.cs index 14c973df5..8208efb39 100644 --- a/src/Dapr.Client/UnsubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/UnsubscribeConfigurationResponse.cs @@ -1,5 +1,4 @@ -using System; -namespace Dapr.Client +namespace Dapr.Client { /// /// Response from an Unsubscribe Configuration call. diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs index 2007ebc5b..e7c2bf34a 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; -using System.Threading; using Dapr.Client; using Microsoft.Extensions.Configuration; diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs index c3d53e072..15ae6ab80 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationExtensions.cs @@ -17,7 +17,6 @@ using Microsoft.Extensions.Configuration; using Dapr.Extensions.Configuration.DaprSecretStore; using System.Linq; -using System.Threading; namespace Dapr.Extensions.Configuration { diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs index 282dc2765..eee3d0b2a 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationSource.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Generic; -using System.Threading; using Dapr.Client; using Microsoft.Extensions.Configuration; diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs index 482d95b97..331156f3e 100644 --- a/src/Dapr.Workflow/WorkflowLoggingService.cs +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -13,7 +13,6 @@ namespace Dapr.Workflow { - using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs b/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs index 60e91b9e3..de27c9300 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/AppWebApplicationFactory.cs @@ -14,7 +14,6 @@ namespace Dapr.Actors.AspNetCore.IntegrationTest { using Dapr.Actors.AspNetCore.IntegrationTest.App; - using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; diff --git a/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs b/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs index aca679993..9c3b55365 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs +++ b/test/Dapr.Actors.AspNetCore.Test/Runtime/DependencyInjectionActorActivatorTests.cs @@ -13,9 +13,7 @@ using System; using System.Threading.Tasks; -using Dapr.Actors.Client; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Dapr.Actors.Runtime diff --git a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs index 1b4a3fb26..2f2bb67db 100644 --- a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs +++ b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs @@ -13,7 +13,6 @@ namespace Dapr.Actors.Test { - using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Builder; using Dapr.Actors.Communication; diff --git a/test/Dapr.Actors.Test/ActorMethodInvocationExceptionTests.cs b/test/Dapr.Actors.Test/ActorMethodInvocationExceptionTests.cs index 5b1e0f526..1311a9394 100644 --- a/test/Dapr.Actors.Test/ActorMethodInvocationExceptionTests.cs +++ b/test/Dapr.Actors.Test/ActorMethodInvocationExceptionTests.cs @@ -15,7 +15,6 @@ namespace Dapr.Actors.Test { using System; using System.IO; - using Dapr.Actors.Communication; using FluentAssertions; using Xunit; diff --git a/test/Dapr.Actors.Test/ActorProxyOptionsTests.cs b/test/Dapr.Actors.Test/ActorProxyOptionsTests.cs index d9ec2cc32..9fab1c32c 100644 --- a/test/Dapr.Actors.Test/ActorProxyOptionsTests.cs +++ b/test/Dapr.Actors.Test/ActorProxyOptionsTests.cs @@ -14,10 +14,6 @@ namespace Dapr.Actors.Client { using System; - using System.Text.Json; - using Dapr.Actors.Builder; - using Dapr.Actors.Client; - using Dapr.Actors.Test; using FluentAssertions; using Xunit; diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs index a6517a6b4..a4e0e4140 100644 --- a/test/Dapr.Actors.Test/ActorStateManagerTest.cs +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -14,17 +14,10 @@ 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; diff --git a/test/Dapr.Actors.Test/ActorUnitTestTests.cs b/test/Dapr.Actors.Test/ActorUnitTestTests.cs index baa52c568..891011de9 100644 --- a/test/Dapr.Actors.Test/ActorUnitTestTests.cs +++ b/test/Dapr.Actors.Test/ActorUnitTestTests.cs @@ -15,9 +15,7 @@ using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; -using Dapr.Actors.Client; using Dapr.Actors.Runtime; -using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; diff --git a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs index 9d5710dfc..9d44e7550 100644 --- a/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs +++ b/test/Dapr.Actors.Test/DaprFormatTimeSpanTests.cs @@ -17,8 +17,6 @@ namespace Dapr.Actors.Test using System; using System.Collections.Generic; using System.IO; - using System.Text; - using System.Text.Json; using System.Threading.Tasks; using Dapr.Actors.Runtime; using Newtonsoft.Json; diff --git a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs index 21c142267..9c42b8fc0 100644 --- a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs +++ b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs @@ -18,13 +18,11 @@ namespace Dapr.Actors.Test using System.Linq; using System.Net; using System.Net.Http; - using System.Security; using System.Security.Authentication; using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using Xunit; - using Dapr.Actors.Communication; /// /// Contains tests for DaprHttpInteractor. diff --git a/test/Dapr.Actors.Test/DaprStateProviderTest.cs b/test/Dapr.Actors.Test/DaprStateProviderTest.cs index 63be89e95..948b14b46 100644 --- a/test/Dapr.Actors.Test/DaprStateProviderTest.cs +++ b/test/Dapr.Actors.Test/DaprStateProviderTest.cs @@ -14,17 +14,10 @@ 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; diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs index c74d0b754..6ebfe1bb2 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs @@ -15,7 +15,6 @@ namespace Dapr.Actors.Test { using System; using System.Buffers; - using System.Collections.Generic; using System.Linq; using System.IO; using System.Text; diff --git a/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs b/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs index f5ee9ae53..87045b295 100644 --- a/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs +++ b/test/Dapr.Actors.Test/Runtime/DefaultActorActivatorTests.cs @@ -13,8 +13,6 @@ using System; using System.Threading.Tasks; -using Dapr.Actors.Client; -using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Dapr.Actors.Runtime diff --git a/test/Dapr.Actors.Test/Serialization/ActorIdDataContractSerializationTest.cs b/test/Dapr.Actors.Test/Serialization/ActorIdDataContractSerializationTest.cs index c7b879d70..dfaccbc19 100644 --- a/test/Dapr.Actors.Test/Serialization/ActorIdDataContractSerializationTest.cs +++ b/test/Dapr.Actors.Test/Serialization/ActorIdDataContractSerializationTest.cs @@ -11,13 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.IO; using System.Runtime.Serialization; -using System.Text.Json; -using System.Text.Json.Serialization; using System.Xml; -using System.Xml.Linq; using FluentAssertions; using Xunit; diff --git a/test/Dapr.Actors.Test/Serialization/TimerInfoJsonConverterTest.cs b/test/Dapr.Actors.Test/Serialization/TimerInfoJsonConverterTest.cs index 5799c172e..264fcb8a0 100644 --- a/test/Dapr.Actors.Test/Serialization/TimerInfoJsonConverterTest.cs +++ b/test/Dapr.Actors.Test/Serialization/TimerInfoJsonConverterTest.cs @@ -13,7 +13,6 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization; using Xunit; #pragma warning disable 0618 diff --git a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs index f9ad63b7b..b4940835c 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs @@ -16,10 +16,8 @@ namespace Dapr.Client using System; using System.Collections.Generic; using System.Net.Http; - using System.Text.Json; using System.Threading; using System.Threading.Tasks; - using Dapr.Client; using Grpc.Net.Client; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index c8a5ff402..9c1f1e005 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -20,7 +20,6 @@ namespace Dapr.AspNetCore.Test using System.Threading.Tasks; using FluentAssertions; using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Xunit; diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index bd807ebae..45cbcc152 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -14,7 +14,6 @@ using System; using System.Text.Json; using Dapr.Client; -using Grpc.Core; using Grpc.Net.Client; using Xunit; diff --git a/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs b/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs index d6b966c97..06572caa5 100644 --- a/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs +++ b/test/Dapr.AspNetCore.Test/StateEntryApplicationModelProviderTest.cs @@ -17,19 +17,11 @@ namespace Dapr.AspNetCore.Test using System.Collections.Generic; using System.Linq; using System.Reflection; - using System.Text.Json; - using System.Threading.Tasks; using Dapr.AspNetCore.Resources; - using Dapr.Client; - using Dapr.Client.Autogen.Grpc.v1; using FluentAssertions; - using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ModelBinding; - using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - using Microsoft.AspNetCore.Mvc.Routing; - using Microsoft.Extensions.DependencyInjection; using Xunit; public class StateEntryApplicationModelProviderTest diff --git a/test/Dapr.Client.Test/ConfigurationApiTest.cs b/test/Dapr.Client.Test/ConfigurationApiTest.cs index 2e96582bd..095024933 100644 --- a/test/Dapr.Client.Test/ConfigurationApiTest.cs +++ b/test/Dapr.Client.Test/ConfigurationApiTest.cs @@ -16,7 +16,6 @@ using Autogenerated = Dapr.Client.Autogen.Grpc.v1; using Xunit; using FluentAssertions; -using System; namespace Dapr.Client.Test { diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 3359c3b48..58663227c 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -11,13 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Generic; using System.Linq; using System.Net.Http.Headers; namespace Dapr.Client.Test { using System; - using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Json; @@ -26,7 +26,6 @@ namespace Dapr.Client.Test using System.Threading; using System.Threading.Tasks; using Dapr.Client; - using FluentAssertions; using Xunit; // Most of the InvokeMethodAsync functionality on DaprClient is non-abstract methods that diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index e3e21dd2b..6ba774e52 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Collections.Immutable; using System.Linq; using System.Net.Http; using System.Text.Json.Serialization; diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index b240605a1..f6ecb5d80 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -22,14 +22,12 @@ namespace Dapr.Client.Test using FluentAssertions; using Google.Protobuf; using Grpc.Core; - using Grpc.Net.Client; using Moq; using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency; using StateConcurrency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConcurrency; using Xunit; using System.Threading; using System.Net.Http; - using System.Text; public class StateApiTest { diff --git a/test/Dapr.Client.Test/TypeConvertersTest.cs b/test/Dapr.Client.Test/TypeConvertersTest.cs index 42a1e4385..1e6b57eab 100644 --- a/test/Dapr.Client.Test/TypeConvertersTest.cs +++ b/test/Dapr.Client.Test/TypeConvertersTest.cs @@ -14,7 +14,6 @@ namespace Dapr.Client.Test { using System.Text.Json; - using Dapr.Client.Autogen.Test.Grpc.v1; using FluentAssertions; using Xunit; diff --git a/test/Dapr.E2E.Test.App.Grpc/Startup.cs b/test/Dapr.E2E.Test.App.Grpc/Startup.cs index 531fb91b3..651f0c76b 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Startup.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Startup.cs @@ -1,23 +1,18 @@ -// ------------------------------------------------------------------------ -// 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.Linq; -using System.Threading.Tasks; +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/test/Dapr.E2E.Test.App/Actors/StateActor.cs b/test/Dapr.E2E.Test.App/Actors/StateActor.cs index 71a952e0f..76e9745ff 100644 --- a/test/Dapr.E2E.Test.App/Actors/StateActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/StateActor.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using System.Text.Json; using System.Threading.Tasks; using Dapr.Actors.Runtime; diff --git a/test/Dapr.E2E.Test.App/Controllers/TestController.cs b/test/Dapr.E2E.Test.App/Controllers/TestController.cs index 4e475c197..1f9274d0e 100644 --- a/test/Dapr.E2E.Test.App/Controllers/TestController.cs +++ b/test/Dapr.E2E.Test.App/Controllers/TestController.cs @@ -15,8 +15,6 @@ namespace Dapr.E2E.Test { using System; using System.Threading.Tasks; - using Dapr; - using Dapr.Client; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 7e7484d54..05c633000 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -13,7 +13,6 @@ 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; @@ -30,7 +29,6 @@ namespace Dapr.E2E.Test using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using System.Threading.Tasks; - using System; using Microsoft.Extensions.Logging; using Serilog; diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs index 184a40448..7a1408a0e 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs @@ -13,7 +13,6 @@ namespace Dapr.E2E.Test { using System; - using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Actors; diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs index da49e6721..78fed3d30 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs @@ -13,15 +13,11 @@ 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 diff --git a/test/Shared/GrpcUtils.cs b/test/Shared/GrpcUtils.cs index 2dfdf8082..fd7ddf151 100644 --- a/test/Shared/GrpcUtils.cs +++ b/test/Shared/GrpcUtils.cs @@ -19,7 +19,6 @@ namespace Dapr using System.Net; using System.Net.Http; using System.Net.Http.Headers; - using System.Text.Json; using System.Threading.Tasks; using Google.Protobuf; using Grpc.Core; From d5af95cd85e850de733cd2425d73b93cf56b3a48 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:20:31 +0100 Subject: [PATCH 27/69] Incremental source generator for actors (#1334) * Samples - Add k8s deployment yaml to DemoActor sample (#1308) * up Signed-off-by: Manuel Menegazzo * Fixed build Signed-off-by: Manuel Menegazzo * Added scripts for image build Signed-off-by: Manuel Menegazzo * Added readme Build and push Docker image Signed-off-by: Manuel Menegazzo * Added demo-actor.yaml Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Updated guide, fixed invocation throw curl Signed-off-by: Manuel Menegazzo * Removed dockerfile, updated readme, removed ps1 and sh scripts Signed-off-by: Manuel Menegazzo * Updated base image Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Manuel Menegazzo * Update demo-actor.yaml Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Manuel Menegazzo * Added overload for DaprClient DI registration (#1289) * Added overload for DaprClient DI registration allowing the consumer to easily use values from injected services (e.g. IConfiguration). Signed-off-by: Whit Waldo * Added supporting unit test Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Co-authored-by: Phillip Hoff Signed-off-by: Manuel Menegazzo * Merge `release-1.13` back into `master` (#1285) * Update protos and related use for Dapr 1.13. (#1236) * Update protos and related use. Signed-off-by: Phillip Hoff * Update Dapr runtime version. Signed-off-by: Phillip Hoff * Init properties. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff * Update artifact action versions. (#1240) Signed-off-by: Phillip Hoff * Make recursive true as default (#1243) Signed-off-by: Shivam Kumar * Fix for secret key transformation in multi-value scenarios (#1274) * Add repro test. Signed-off-by: Phillip Hoff * Fix for secret key transformation in multi-value scenarios. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff * Update Dapr version numbers used during testing. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Co-authored-by: Shivam Kumar Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Whit Waldo Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Co-authored-by: Whit Waldo Co-authored-by: Phillip Hoff Co-authored-by: Shivam Kumar Signed-off-by: Manuel Menegazzo * Aligned nuget version Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Debug profile added Signed-off-by: Manuel Menegazzo * Updated implementation Signed-off-by: Manuel Menegazzo * Emitted DAPR001 Diagnostic warning Signed-off-by: Manuel Menegazzo * Added DAPR002 diagnostic Signed-off-by: Manuel Menegazzo * Cleaun Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Added summaries Signed-off-by: Manuel Menegazzo * Added base interface to ActorClient Signed-off-by: Manuel Menegazzo * Updated Signed-off-by: Manuel Menegazzo * Added ctor Signed-off-by: Manuel Menegazzo * Added nullable directive Signed-off-by: Manuel Menegazzo * Added null check for actorproxy ctor parameter Signed-off-by: Manuel Menegazzo * Moved DiagnoticException in a dedicate cs file Signed-off-by: Manuel Menegazzo * Moved generator costants to dedicated class Signed-off-by: Manuel Menegazzo * Added ActorReference creation from the ActorBase class informations (#1277) * Handled creation of ActorReference from Actor base class Signed-off-by: Manuel Menegazzo * Updated null check Signed-off-by: Manuel Menegazzo * Added unit test for GetActorReference from null actore and actor proxy Signed-off-by: Manuel Menegazzo * Added test for ActorReference created inside Actor implementation Signed-off-by: Manuel Menegazzo * Updated description Signed-off-by: Manuel Menegazzo * Fixed test method naming Signed-off-by: Manuel Menegazzo * Added unit test for exception generated in case the type is not convertible to an ActorReference Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo * Added overload to support SDK supplying query string on invoked URL (#1310) * Refactored extensions and their tests into separate directories Signed-off-by: Whit Waldo * Added overload to method invocation to allow query string parameters to be passed in via the SDK instead of being uncermoniously added to the end of the produced HttpRequestMessage URI Signed-off-by: Whit Waldo * Added unit tests to support implementation Signed-off-by: Whit Waldo * Marking HttpExtensions as internal to prevent external usage and updating to work against Uri instead of HttpRequestMessage. Signed-off-by: Whit Waldo * Updated unit tests to match new extension purpose Signed-off-by: Whit Waldo * Resolved an ambiguous method invocation wherein it was taking the query string and passing it as the payload for a request. Removed the offending method and reworked the remaining configurations so there's no API impact. Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Manuel Menegazzo * Fixed actorProxy argument null check Signed-off-by: Manuel Menegazzo * Moved ActorClientDesciptor into separta cs file Signed-off-by: Manuel Menegazzo * Moved textual templates to dedicated class Signed-off-by: Manuel Menegazzo * Updated comments, property names Signed-off-by: Manuel Menegazzo * Added argument null check to SyntaxFactoryHelpers Signed-off-by: Manuel Menegazzo * Added comments Signed-off-by: Manuel Menegazzo * Removed obsolete testing packages https://github.com/dotnet/roslyn-sdk/blob/main/src/Microsoft.CodeAnalysis.Testing/README.md#obsolete-packages Signed-off-by: Manuel Menegazzo * Adapted existing unit test to new source generated code Signed-off-by: Manuel Menegazzo * Up Signed-off-by: Manuel Menegazzo * Added tests for SyntaxFactoryHelpers Signed-off-by: Manuel Menegazzo * Updated generation of ArgumentNullException Signed-off-by: Manuel Menegazzo * Updated nullability Signed-off-by: Manuel Menegazzo * Fixed internal methods tests Signed-off-by: Manuel Menegazzo * Added test to IEnumerableExtensions Signed-off-by: Manuel Menegazzo * Unittested GetSyntaxKinds from Accessibility Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Updated assignment implementation of ctor body Signed-off-by: Manuel Menegazzo * Improved unit test Signed-off-by: Manuel Menegazzo * Added implementation of method generation Signed-off-by: Manuel Menegazzo * Fixed ArgumentNullException invocation Signed-off-by: Manuel Menegazzo * Added test for NameOfExpression Signed-off-by: Manuel Menegazzo * Fixed ActorProxy method invocation Signed-off-by: Manuel Menegazzo * Simplified proxy argument definition Signed-off-by: Manuel Menegazzo * Explicit generic arguments of the proxy call during generation Signed-off-by: Manuel Menegazzo * Handled cancellation token with default value Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Configured eol used in NormalizeWhitespace function Signed-off-by: Manuel Menegazzo * Normalized expected source Signed-off-by: Manuel Menegazzo * Moved to constat the ActorProxyTypeName Signed-off-by: Manuel Menegazzo * Fix typo Signed-off-by: Manuel Menegazzo * Created ActorProxyInvokeMethodAsync SyntaxFactoryHelper Signed-off-by: Manuel Menegazzo * Removed custom concat implementation Signed-off-by: Manuel Menegazzo * fix (#1329) Signed-off-by: Hannah Hunter Signed-off-by: Manuel Menegazzo * link to non-dapr endpoint howto (#1335) Signed-off-by: Hannah Hunter Signed-off-by: Manuel Menegazzo * Merge 1.14 release branch back into `master`. (#1337) Signed-off-by: Manuel Menegazzo * Fixed merge errors Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo * Updated some summaries Signed-off-by: Manuel Menegazzo * Added some missing summaries Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Improved some summary text Signed-off-by: Manuel Menegazzo * Improved summaries Signed-off-by: Manuel Menegazzo * Handled review requests Signed-off-by: Manuel Menegazzo * Changed SyntaxFactoryHelpers accessor to internal Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Whit Waldo Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Signed-off-by: Hannah Hunter Co-authored-by: Whit Waldo Co-authored-by: Phillip Hoff Co-authored-by: Shivam Kumar Co-authored-by: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 2 +- .../ActorClient/ActorClient.csproj | 41 +- .../ActorClient/IClientActor.cs | 2 +- .../ActorClientGenerator.cs | 454 +++++++++--------- .../AnalyzerReleases.Shipped.md | 8 + .../AnalyzerReleases.Unshipped.md | 3 + src/Dapr.Actors.Generators/Constants.cs | 38 ++ .../Dapr.Actors.Generators.csproj | 64 +-- ...CancellationTokensMustBeTheLastArgument.cs | 25 + ...tOptionallyFollowedByACancellationToken.cs | 25 + .../DiagnosticsException.cs | 25 + .../Extensions/IEnumerableExtensions.cs | 34 ++ .../Helpers/SyntaxFactoryHelpers.cs | 159 ++++++ .../Models/ActorClientDescriptor.cs | 46 ++ .../Properties/launchSettings.json | 8 + src/Dapr.Actors.Generators/Templates.cs | 87 ++++ .../ActorClientGeneratorTests.cs | 292 +++++------ .../CSharpSourceGeneratorVerifier.cs | 23 +- .../Dapr.Actors.Generators.Test.csproj | 5 +- .../Extensions/IEnumerableExtensionsTests.cs | 52 ++ .../Helpers/SyntaxFactoryHelpersTests.cs | 133 +++++ 21 files changed, 1090 insertions(+), 436 deletions(-) create mode 100644 src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Actors.Generators/Constants.cs create mode 100644 src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs create mode 100644 src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs create mode 100644 src/Dapr.Actors.Generators/DiagnosticsException.cs create mode 100644 src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs create mode 100644 src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs create mode 100644 src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs create mode 100644 src/Dapr.Actors.Generators/Properties/launchSettings.json create mode 100644 src/Dapr.Actors.Generators/Templates.cs create mode 100644 test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs create mode 100644 test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 3c1459b5d..332939a5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/examples/GeneratedActor/ActorClient/ActorClient.csproj b/examples/GeneratedActor/ActorClient/ActorClient.csproj index 73b5c2027..88f75663d 100644 --- a/examples/GeneratedActor/ActorClient/ActorClient.csproj +++ b/examples/GeneratedActor/ActorClient/ActorClient.csproj @@ -1,22 +1,29 @@ - + - - Exe - net6 - 10.0 - enable - enable - + + Exe + net6 + 10.0 + enable + enable - - - - + + true + + + + + - - - + + + + + + + + diff --git a/examples/GeneratedActor/ActorClient/IClientActor.cs b/examples/GeneratedActor/ActorClient/IClientActor.cs index c5c732cb9..c687ecf03 100644 --- a/examples/GeneratedActor/ActorClient/IClientActor.cs +++ b/examples/GeneratedActor/ActorClient/IClientActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // 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. diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index f95fc4224..001604d53 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -11,7 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Immutable; +using Dapr.Actors.Generators.Diagnostics; +using Dapr.Actors.Generators.Extensions; +using Dapr.Actors.Generators.Helpers; +using Dapr.Actors.Generators.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Dapr.Actors.Generators; @@ -20,283 +26,265 @@ namespace Dapr.Actors.Generators; /// Generates strongly-typed actor clients that use the non-remoting actor proxy. /// [Generator] -public sealed class ActorClientGenerator : ISourceGenerator +public sealed class ActorClientGenerator : IIncrementalGenerator { - 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 + /// + public void Initialize(IncrementalGeneratorInitializationContext context) { - private readonly List models = new(); - - public IEnumerable Models => this.models; - - #region ISyntaxContextReceiver Members - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + // Register the source output that generates the attribute definitions for ActorMethodAttribute and GenerateActorClientAttribute. + context.RegisterPostInitializationOutput(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 + context.AddSource( + $"{Constants.ActorMethodAttributeFullTypeName}.g.cs", + Templates.ActorMethodAttributeSourceText(Constants.GeneratorsNamespace)); + + context.AddSource( + $"{Constants.GenerateActorClientAttributeFullTypeName}.g.cs", + Templates.GenerateActorClientAttributeSourceText(Constants.GeneratorsNamespace)); + }); + + // Register the value provider that triggers the generation of actor clients when detecting the GenerateActorClientAttribute. + IncrementalValuesProvider actorClientsToGenerate = context.SyntaxProvider + .ForAttributeWithMetadataName( + Constants.GenerateActorClientAttributeFullTypeName, + predicate: static (_, _) => true, + transform: static (gasc, cancellationToken) => CreateActorClientDescriptor(gasc, cancellationToken)); + + // Register the source output that generates the actor clients. + context.RegisterSourceOutput(actorClientsToGenerate, GenerateActorClientCode); } - #region ISourceGenerator Members - - /// - public void Execute(GeneratorExecutionContext context) + /// + /// Returns the descriptor for the actor client to generate. + /// + /// Current generator syntax context passed from generator pipeline. + /// Cancellation token used to interrupt the generation. + /// Returns the descriptor of actor client to generate. + private static ActorClientDescriptor CreateActorClientDescriptor( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken) { - 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 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(); + // Return the attribute data of GenerateActorClientAttribute, which is the attribute that triggered this generator + // and is expected to be the only attribute in the list of matching attributes. + var attributeData = context.Attributes.Single(); - var methodImplementations = String.Join("\n", members.Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol))); + var actorInterfaceSymbol = (INamedTypeSymbol)context.TargetSymbol; - var source = $@" -// + // Use the namespace specified in the GenerateActorClientAttribute, or the namespace of the actor interface if not specified. + var namespaceName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Namespace").Value.Value?.ToString() + ?? actorInterfaceSymbol.ContainingNamespace.ToDisplayString(); -namespace {namespaceName} -{{ - {accessibility} sealed class {clientTypeName} : {fullyQualifiedActorInterfaceTypeName} - {{ - private readonly Dapr.Actors.Client.ActorProxy actorProxy; + // Use the name specified in the GenerateActorClientAttribute, or the name of the actor interface with a "Client" suffix if not specified. + var clientName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() + ?? $"{(actorInterfaceSymbol.Name.StartsWith("I") ? actorInterfaceSymbol.Name.Substring(1) : actorInterfaceSymbol.Name)}Client"; - public {clientTypeName}(Dapr.Actors.Client.ActorProxy actorProxy) - {{ - this.actorProxy = actorProxy; - }} + // Actor member to generate the client for. + var members = actorInterfaceSymbol + .GetMembers() + .OfType() + .Where(m => m.MethodKind == MethodKind.Ordinary) + .ToImmutableArray(); - {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); - } - } - } + return new ActorClientDescriptor + { + NamespaceName = namespaceName, + ClientTypeName = clientName, + Methods = members, + Accessibility = actorInterfaceSymbol.DeclaredAccessibility, + InterfaceType = actorInterfaceSymbol, + Compilation = context.SemanticModel.Compilation, + }; } - /// - public void Initialize(GeneratorInitializationContext context) + /// + /// Generates the actor client code based on the specified descriptor. + /// + /// Context passed from the source generator when it has registered an output. + /// Descriptor of actor client to generate. + /// Throws when one or more required symbols assembly are missing. + private static void GenerateActorClientCode(SourceProductionContext context, ActorClientDescriptor descriptor) { - /* - while (!Debugger.IsAttached) + try { - System.Threading.Thread.Sleep(500); - } - */ - - context.RegisterForPostInitialization( - i => - { - i.AddSource($"{ActorMethodAttributeFullTypeName}.g.cs", ActorMethodAttributeText); - i.AddSource($"{GenerateActorClientAttributeFullTypeName}.g.cs", GenerateActorClientAttributeText); - }); + var actorMethodAttributeSymbol = descriptor.Compilation.GetTypeByMetadataName(Constants.ActorMethodAttributeFullTypeName) + ?? throw new InvalidOperationException("Could not find ActorMethodAttribute type."); - context.RegisterForSyntaxNotifications(() => new ActorInterfaceSyntaxReceiver()); - } + var cancellationTokenSymbol = descriptor.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken") + ?? throw new InvalidOperationException("Could not find CancellationToken type."); - #endregion + var actorClientBaseInterface = SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(descriptor.InterfaceType.ToString())); + var autoGeneratedComment = SyntaxFactory.Comment("// "); + var nullableAnnotation = SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true)); + var actorProxyTypeSyntax = SyntaxFactory.ParseTypeName(Constants.ActorProxyTypeName); - private static string GetClientAccessibility(INamedTypeSymbol interfaceSymbol) - { - return interfaceSymbol.DeclaredAccessibility switch + // Generate the actor proxy field to store the actor proxy instance. + var actorProxyFieldDeclaration = SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(actorProxyTypeSyntax) + .WithVariables(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier("actorProxy"))))) + .WithModifiers(SyntaxFactory.TokenList(new[] + { + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword) + })); + + // Generate the constructor for the actor client. + var actorCtor = SyntaxFactory.ConstructorDeclaration(SyntaxFactory.Identifier(descriptor.ClientTypeName)) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Parameter(SyntaxFactory.Identifier("actorProxy")).WithType(actorProxyTypeSyntax) + }))) + .WithBody(SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactoryHelpers.ThrowIfArgumentNull("actorProxy"), + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy")), + SyntaxFactory.IdentifierName("actorProxy")) + ), + }))); + + var actorMethods = descriptor.Methods + .OrderBy(member => member.DeclaredAccessibility) + .ThenBy(member => member.Name) + .Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol)); + + var actorMembers = new List() + .Append(actorProxyFieldDeclaration) + .Append(actorCtor) + .Concat(actorMethods); + + var actorClientClassModifiers = new List() + .Concat(SyntaxFactoryHelpers.GetSyntaxKinds(descriptor.Accessibility)) + .Append(SyntaxKind.SealedKeyword) + .Select(sk => SyntaxFactory.Token(sk)); + + var actorClientClassDeclaration = SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); + + var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(descriptor.NamespaceName)) + .WithMembers(SyntaxFactory.List(new[] { actorClientClassDeclaration })) + .WithLeadingTrivia(SyntaxFactory.TriviaList(new[] { + autoGeneratedComment, + nullableAnnotation, + })); + + var compilationOutput = SyntaxFactory.CompilationUnit() + .WithMembers(SyntaxFactory.SingletonList(namespaceDeclaration)) + .NormalizeWhitespace() + .ToFullString(); + + context.AddSource($"{descriptor.FullyQualifiedTypeName}.g.cs", compilationOutput); + } + catch (DiagnosticsException e) { - 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; + foreach (var diagnostic in e.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + } } - private static string GenerateMethodImplementation(IMethodSymbol method, INamedTypeSymbol generateActorClientAttributeSymbol, INamedTypeSymbol cancellationTokenSymbol) + /// + /// Generates the method implementation for the specified method. + /// + /// + /// MethodSymbol extracted from the actor interface representing the method to generate. + /// + /// + /// ActorMethodAttribute symbol used to extract the original actor method name to use when making runtime calls. + /// + /// Symbol used to search the position of cancellationToken between method parameters. + /// Returns a of the generated method. + private static MethodDeclarationSyntax 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; + var diagnostics = new List(); 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()) - }); + diagnostics.Add(CancellationTokensMustBeTheLastArgument.CreateDiagnostic(cancellationTokenParameter)); } - if ((method.Parameters.Length > 1 && cancellationTokenIndex == -1) - || (method.Parameters.Length > 2)) + 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()) - }); + diagnostics.Add(MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.CreateDiagnostic(method)); } - 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) + // If there are any diagnostics, throw an exception to report them and stop the generation. + if (diagnostics.Any()) { - argumentDefinitions = argumentDefinitions + " = default"; + throw new DiagnosticsException(diagnostics); } - string argumentList = String.Join(", ", new[] { $@"""{actualMethodName}""" }.Concat(method.Parameters.Select(p => p.Name))); + // Get the ActorMethodAttribute data for the method, if it exists. + var attributeData = method.GetAttributes() + .SingleOrDefault(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); - string templateArgs = - returnTypeArgument is not null - ? $"<{(requestParameter is not null ? $"{requestParameter.Type}, " : "")}{returnTypeArgument}>" - : ""; + // Generate the method name to use for the Dapr actor method invocation, using the Name property of ActorMethodAttribute if specified, + // or the original method name otherwise. + var daprMethodName = attributeData?.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() ?? method.Name; - return - $@"public {method.ReturnType} {method.Name}({argumentDefinitions}) - {{ - return this.actorProxy.InvokeMethodAsync{templateArgs}({argumentList}); - }}"; - } -} + var methodModifiers = new List() + .Concat(SyntaxFactoryHelpers.GetSyntaxKinds(method.DeclaredAccessibility)) + .Select(sk => SyntaxFactory.Token(sk)); -internal static class Extensions -{ - public static int IndexOf(this IEnumerable source, Func predicate) - { - int index = 0; + // Define the parameters to pass to the actor proxy method invocation. + // Exclude the CancellationToken parameter if it exists, because it need to be handled separately. + var methodParameters = method.Parameters + .Where(p => p.Type is not INamedTypeSymbol { Name: "CancellationToken" }) + .Select(p => SyntaxFactory.Parameter(SyntaxFactory.Identifier(p.Name)).WithType(SyntaxFactory.ParseTypeName(p.Type.ToString()))); - foreach (var item in source) + // Append the CancellationToken parameter if it exists, handling the case where it is optional and has no default value. + if (cancellationTokenParameter is not null) { - if (predicate(item)) + if (cancellationTokenParameter.IsOptional + && cancellationTokenParameter.HasExplicitDefaultValue + && cancellationTokenParameter.ExplicitDefaultValue is null) { - return index; + methodParameters = methodParameters.Append( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(cancellationTokenParameter.Name)) + .WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression))) + .WithType(SyntaxFactory.ParseTypeName(cancellationTokenParameter.Type.ToString()))); + } + else + { + methodParameters = methodParameters.Append( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(cancellationTokenParameter.Name)) + .WithType(SyntaxFactory.ParseTypeName(cancellationTokenParameter.Type.ToString()))); } - - index++; } - return -1; - } -} + // Extract the return type of the original method. + var methodReturnType = (INamedTypeSymbol)method.ReturnType; -internal sealed class DiagnosticsException : Exception -{ - public DiagnosticsException(IEnumerable diagnostics) - : base(String.Join("\n", diagnostics.Select(d => d.ToString()))) - { - this.Diagnostics = diagnostics.ToArray(); + // Generate the method implementation. + var generatedMethod = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName(method.ReturnType.ToString()), method.Name) + .WithModifiers(SyntaxFactory.TokenList(methodModifiers)) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(methodParameters))) + .WithBody(SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactory.ReturnStatement(SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy")), + daprMethodName, + method.Parameters, + methodReturnType.TypeArguments + )), + }))); + + return generatedMethod; } - - public IEnumerable Diagnostics { get; } } diff --git a/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..62b61ac2c --- /dev/null +++ b/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.14 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR0001| Usage | Error | Cancellation tokens must be the last argument +DAPR0002| Usage | Error | Only methods with a single argument or a single argument followed by a cancellation token are supported \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..b1b99aaf2 --- /dev/null +++ b/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Actors.Generators/Constants.cs b/src/Dapr.Actors.Generators/Constants.cs new file mode 100644 index 000000000..392def4ef --- /dev/null +++ b/src/Dapr.Actors.Generators/Constants.cs @@ -0,0 +1,38 @@ +namespace Dapr.Actors.Generators +{ + /// + /// Constants used by the code generator. + /// + internal static class Constants + { + /// + /// The namespace used by the generated code. + /// + public const string GeneratorsNamespace = "Dapr.Actors.Generators"; + + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; + + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; + + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeTypeName = "GenerateActorClientAttribute"; + + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttributeTypeName; + + /// + /// Actor proxy type name. + /// + public const string ActorProxyTypeName = "Dapr.Actors.Client.ActorProxy"; + } +} diff --git a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj index 370d422f1..b1f73383a 100644 --- a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -1,45 +1,55 @@ - + - - enable - enable - + + enable + enable + true + - - true - + + true + - - - - netstandard2.0 - + + + netstandard2.0 + - - false + + false - - true + + true - - false + + false - - This package contains source generators for interacting with Actor services using Dapr. - $(PackageTags);Actors - + + This package contains source generators for interacting with Actor services using Dapr. + $(PackageTags);Actors + - - - - + + + + + + + + + + + + + diff --git a/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs new file mode 100644 index 000000000..376bb360f --- /dev/null +++ b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Diagnostics +{ + internal static class CancellationTokensMustBeTheLastArgument + { + public const string DiagnosticId = "DAPR0001"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Cancellation tokens must be the last argument"; + public const string Category = "Usage"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } +} diff --git a/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs new file mode 100644 index 000000000..c82b20630 --- /dev/null +++ b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Diagnostics +{ + internal static class MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken + { + public const string DiagnosticId = "DAPR0002"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Only methods with a single argument or a single argument followed by a cancellation token are supported"; + public const string Category = "Usage"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } +} diff --git a/src/Dapr.Actors.Generators/DiagnosticsException.cs b/src/Dapr.Actors.Generators/DiagnosticsException.cs new file mode 100644 index 000000000..d196f8484 --- /dev/null +++ b/src/Dapr.Actors.Generators/DiagnosticsException.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators +{ + /// + /// Exception thrown when diagnostics are encountered during code generation. + /// + internal sealed class DiagnosticsException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// List of diagnostics generated. + public DiagnosticsException(IEnumerable diagnostics) + : base(string.Join("\n", diagnostics.Select(d => d.ToString()))) + { + this.Diagnostics = diagnostics.ToArray(); + } + + /// + /// Diagnostics encountered during code generation. + /// + public ICollection Diagnostics { get; } + } +} diff --git a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs new file mode 100644 index 000000000..6b45e86f3 --- /dev/null +++ b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,34 @@ +namespace Dapr.Actors.Generators.Extensions +{ + internal static class IEnumerableExtensions + { + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) + { + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + int index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } + } +} diff --git a/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs new file mode 100644 index 000000000..36df7b280 --- /dev/null +++ b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs @@ -0,0 +1,159 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Generators.Helpers +{ + /// + /// Syntax factory helpers for generating syntax. + /// + internal static partial class SyntaxFactoryHelpers + { + /// + /// Generates a syntax for an based on the given argument name. + /// + /// Name of the argument that generated the exception. + /// Returns used to throw an . + public static ThrowExpressionSyntax ThrowArgumentNullException(string argumentName) + { + return SyntaxFactory.ThrowExpression( + SyntaxFactory.Token(SyntaxKind.ThrowKeyword), + SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.Token(SyntaxKind.NewKeyword), + SyntaxFactory.ParseTypeName("System.ArgumentNullException"), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Argument(NameOfExpression(argumentName)) + })), + default + ) + ); + } + + /// + /// Generates a syntax for null check for the given argument name. + /// + /// Name of the argument whose null check is to be generated. + /// Returns representing an argument null check. + public static IfStatementSyntax ThrowIfArgumentNull(string argumentName) + { + return SyntaxFactory.IfStatement( + SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + SyntaxFactory.IdentifierName(argumentName), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ), + SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(argumentName)) + })) + ); + } + + /// + /// Generates a syntax for nameof expression for the given argument name. + /// + /// Name of the argument from which the syntax is to be generated. + /// Return a representing a NameOf expression. + public static ExpressionSyntax NameOfExpression(string argumentName) + { + var nameofIdentifier = SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.NameOfKeyword, + "nameof", + "nameof", + SyntaxFactory.TriviaList()); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName(nameofIdentifier), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(argumentName)) + })) + ); + } + + /// + /// Generates the invocation syntax to call a remote method with the actor proxy. + /// + /// Member syntax to access actorProxy member. + /// Name of remote method to invoke. + /// Remote method parameters. + /// Return types of remote method invocation. + /// Returns the representing a call to the actor proxy. + public static InvocationExpressionSyntax ActorProxyInvokeMethodAsync( + MemberAccessExpressionSyntax actorProxyMemberSyntax, + string remoteMethodName, + IEnumerable remoteMethodParameters, + IEnumerable remoteMethodReturnTypes) + { + // Define the type arguments to pass to the actor proxy method invocation. + var proxyInvocationTypeArguments = new List() + .Concat(remoteMethodParameters + .Where(p => p.Type is not { Name: "CancellationToken" }) + .Select(p => SyntaxFactory.ParseTypeName(p.Type.ToString()))) + .Concat(remoteMethodReturnTypes + .Select(a => SyntaxFactory.ParseTypeName(a.OriginalDefinition.ToString()))); + + // Define the arguments to pass to the actor proxy method invocation. + var proxyInvocationArguments = new List() + // Name of remote method to invoke. + .Append(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(remoteMethodName)))) + // Actor method arguments, including the CancellationToken if it exists. + .Concat(remoteMethodParameters.Select(p => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Name)))); + + // If the invocation has return types or input parameters, we need to use the generic version of the method. + SimpleNameSyntax invokeAsyncSyntax = proxyInvocationTypeArguments.Any() + ? SyntaxFactory.GenericName( + SyntaxFactory.Identifier("InvokeMethodAsync"), + SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(proxyInvocationTypeArguments))) + : SyntaxFactory.IdentifierName("InvokeMethodAsync"); + + // Generate the invocation syntax. + var generatedInvocationSyntax = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + actorProxyMemberSyntax, + invokeAsyncSyntax + )) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(proxyInvocationArguments))); + + return generatedInvocationSyntax; + } + + /// + /// Returns the for the specified accessibility. + /// + /// Accessibility to convert into a . + /// Returns the collection of representing the given accessibility. + /// Throws when un unexpected accessibility is passed. + public static ICollection GetSyntaxKinds(Accessibility accessibility) + { + var syntaxKinds = new List(); + + switch (accessibility) + { + case Accessibility.Public: + syntaxKinds.Add(SyntaxKind.PublicKeyword); + break; + case Accessibility.Internal: + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + case Accessibility.Private: + syntaxKinds.Add(SyntaxKind.PrivateKeyword); + break; + case Accessibility.Protected: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + break; + case Accessibility.ProtectedAndInternal: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + default: + throw new InvalidOperationException("Unexpected accessibility"); + } + + return syntaxKinds; + } + } +} diff --git a/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs new file mode 100644 index 000000000..e1f54fac4 --- /dev/null +++ b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Models +{ + /// + /// Describes an actor client to generate. + /// + internal record class ActorClientDescriptor : IEquatable + { + /// + /// Gets or sets the symbol representing the actor interface. + /// + public INamedTypeSymbol InterfaceType { get; set; } = null!; + + /// + /// Accessibility of the generated client. + /// + public Accessibility Accessibility { get; set; } + + /// + /// Namespace of the generated client. + /// + public string NamespaceName { get; set; } = string.Empty; + + /// + /// Name of the generated client. + /// + public string ClientTypeName { get; set; } = string.Empty; + + /// + /// Fully qualified type name of the generated client. + /// + public string FullyQualifiedTypeName => $"{NamespaceName}.{ClientTypeName}"; + + /// + /// Methods to generate in the client. + /// + public ImmutableArray Methods { get; set; } = Array.Empty().ToImmutableArray(); + + /// + /// Compilation to use for generating the client. + /// + public Compilation Compilation { get; set; } = null!; + } +} diff --git a/src/Dapr.Actors.Generators/Properties/launchSettings.json b/src/Dapr.Actors.Generators/Properties/launchSettings.json new file mode 100644 index 000000000..f146e6195 --- /dev/null +++ b/src/Dapr.Actors.Generators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Debug": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\examples\\GeneratedActor\\ActorClient\\ActorClient.csproj" + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Templates.cs b/src/Dapr.Actors.Generators/Templates.cs new file mode 100644 index 000000000..6cc4c9f87 --- /dev/null +++ b/src/Dapr.Actors.Generators/Templates.cs @@ -0,0 +1,87 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Dapr.Actors.Generators +{ + /// + /// Templates for generating source code. + /// + internal static partial class Templates + { + /// + /// Returns the for the ActorMethodAttribute. + /// + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText ActorMethodAttributeSourceText(string destinationNamespace) + { + if (destinationNamespace is null) + { + throw new ArgumentNullException(nameof(destinationNamespace)); + } + + var source = $@" +// + +#nullable enable + +using System; + +namespace {destinationNamespace} +{{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class {Constants.ActorMethodAttributeTypeName} : Attribute + {{ + public string? Name {{ get; set; }} + }} +}}"; + + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); + } + + /// + /// Returns the for the GenerateActorClientAttribute. + /// + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText GenerateActorClientAttributeSourceText(string destinationNamespace) + { + if (destinationNamespace is null) + { + throw new ArgumentNullException(nameof(destinationNamespace)); + } + + string source = $@" +// + +#nullable enable + +using System; + +namespace {destinationNamespace} +{{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class {Constants.GenerateActorClientAttributeTypeName} : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} +}}"; + + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs index ce4c0accd..4c0ef194e 100644 --- a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // 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. @@ -21,43 +21,40 @@ namespace Dapr.Actors.Generators; 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 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 = ( + Path.Combine("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 = ( + Path.Combine("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) { @@ -77,7 +74,9 @@ private static VerifyCS.Test CreateTest(string originalSource, string? generated 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))); + test.TestState.GeneratedSources.Add(( + Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", generatedName), + SourceText.From(generatedSource, Encoding.UTF8))); } return test; @@ -97,20 +96,22 @@ public interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -119,8 +120,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -139,20 +139,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -161,8 +163,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -181,20 +182,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { internal sealed class MyTestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -203,8 +206,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } @@ -223,20 +225,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace MyTest { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -245,8 +249,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -269,17 +272,20 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -288,8 +294,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""MyTestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -313,27 +318,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod(Test.TestValue value) { - return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); + return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -354,20 +361,22 @@ public interface ITestActor { Task TestMethodAsync(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -376,8 +385,7 @@ public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) return this.actorProxy.InvokeMethodAsync(""TestMethodAsync""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -400,20 +408,22 @@ public interface ITestActor { Task TestMethodAsync(TestRequestValue value); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -422,8 +432,7 @@ public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -443,20 +452,22 @@ public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -465,8 +476,7 @@ public System.Threading.Tasks.Task TestMethodAsync(System.Threading.Cancellation return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -486,20 +496,22 @@ public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken = default); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -508,8 +520,7 @@ public System.Threading.Tasks.Task TestMethodAsync(System.Threading.Cancellation return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -534,27 +545,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken) { - return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -579,27 +592,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(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); + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -621,15 +636,14 @@ 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.")); + .WithMessage("Cancellation tokens must be the last argument")); await test.RunAsync(); } @@ -651,15 +665,14 @@ 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.")); + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported")); await test.RunAsync(); } @@ -681,16 +694,15 @@ 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.")); + .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/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index 2b1046e1a..c64fd3427 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // 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. @@ -16,28 +16,25 @@ 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() + where TSourceGenerator : IIncrementalGenerator, new() { -#pragma warning disable CS0618 // Type or member is obsolete - public class Test : CSharpSourceGeneratorTest -#pragma warning restore CS0618 // Type or member is obsolete + public class Test : CSharpSourceGeneratorTest { public Test() { int frameworkVersion = - #if NET6_0 +#if NET6_0 6; - #elif NET7_0 +#elif NET7_0 7; - #elif NET8_0 +#elif NET8_0 8; - #endif +#endif // // NOTE: Ordinarily we'd use the following: @@ -58,10 +55,10 @@ public Test() protected override CompilationOptions CreateCompilationOptions() { - var compilationOptions = base.CreateCompilationOptions(); + var compilationOptions = base.CreateCompilationOptions(); - return compilationOptions - .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + return compilationOptions + .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); } public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 80a79cafe..9e9a9e4db 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -13,9 +13,7 @@ - - - + @@ -27,7 +25,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs new file mode 100644 index 000000000..97dbcfe1e --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs @@ -0,0 +1,52 @@ +using Dapr.Actors.Generators.Extensions; + +namespace Dapr.Actors.Generators.Test.Extensions +{ + public class IEnumerableExtensionsTests + { + [Fact] + public void IndexOf_WhenPredicateIsNull_ThrowsArgumentNullException() + { + // Arrange + var source = new[] { 1, 2, 3, 4, 5 }; + Func predicate = null!; + + // Act + Action act = () => source.IndexOf(predicate); + + // Assert + Assert.Throws(act); + } + + [Theory] + [InlineData(new int[] { }, 3, -1)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 6, -1)] + public void IndexOf_WhenItemDoesNotExist_ReturnsMinusOne(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } + + [Theory] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 3, 2)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 1, 0)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 5, 4)] + public void IndexOf_WhenItemExists_ReturnsIndexOfItem(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs new file mode 100644 index 000000000..807bd7469 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs @@ -0,0 +1,133 @@ +using Dapr.Actors.Generators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Dapr.Actors.Generators.Test.Helpers +{ + public class SyntaxFactoryHelpersTests + { + [Fact] + public void ThrowArgumentNullException_GenerateThrowArgumentNullExceptionSyntaxWithGivenArgumentName() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"throw new System.ArgumentNullException(nameof(arg0));"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactory.ExpressionStatement(SyntaxFactoryHelpers.ThrowArgumentNullException(argumentName)) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void ThrowIfArgumentNullException_GivesNullCheckSyntaxWithGivenArgumentName() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"if (arg0 is null) +{{ + throw new System.ArgumentNullException(nameof(arg0)); +}}"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.ThrowIfArgumentNull(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void ActorProxyInvokeMethodAsync_WithoutReturnTypeAndParamters_ReturnNonGenericInvokeMethodAsync() + { + // Arrange + var remoteMethodName = "RemoteMethodToCall"; + var remoteMethodParameters = Array.Empty(); + var remoteMethodReturnTypes = Array.Empty(); + var actorProxMemberAccessSyntax = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy") + ); + var expectedSource = $@"this.actorProxy.InvokeMethodAsync(""RemoteMethodToCall"")"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( + actorProxMemberAccessSyntax, + remoteMethodName, + remoteMethodParameters, + remoteMethodReturnTypes) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); ; + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void NameOfExpression() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"nameof(arg0)"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.NameOfExpression(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Theory] + [InlineData(Accessibility.Public, new[] { SyntaxKind.PublicKeyword })] + [InlineData(Accessibility.Internal, new[] { SyntaxKind.InternalKeyword })] + [InlineData(Accessibility.Private, new[] { SyntaxKind.PrivateKeyword })] + [InlineData(Accessibility.Protected, new[] { SyntaxKind.ProtectedKeyword })] + [InlineData(Accessibility.ProtectedAndInternal, new[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword })] + public void GetSyntaxKinds_GenerateSyntaxForGivenAccessibility(Accessibility accessibility, ICollection expectedSyntaxKinds) + { + // Arrange + + // Act + var generatedSyntaxKinds = SyntaxFactoryHelpers.GetSyntaxKinds(accessibility); + + // Assert + foreach (var expectedSyntaxKind in expectedSyntaxKinds) + { + Assert.Contains(expectedSyntaxKind, generatedSyntaxKinds); + } + + Assert.Equal(expectedSyntaxKinds.Count, generatedSyntaxKinds.Count); + } + } +} From 03995c7640e93a6256f2345beb48d8ba2cbb94e2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 1 Nov 2024 12:08:59 -0500 Subject: [PATCH 28/69] Add .NET client for Dapr Jobs API (#1384) * Package addition + updates Signed-off-by: Whit Waldo * Added Dapr.Jobs project Signed-off-by: Whit Waldo * Initial commit - unable to proceed without update on master from streaming sub PR Signed-off-by: Whit Waldo * Added class to Dapr.Common, fixed compilation errors Signed-off-by: Whit Waldo * Added unit tests for Dapr.Common enum extensions Signed-off-by: Whit Waldo * Added unit tests Signed-off-by: Whit Waldo * Added missing copyright header Signed-off-by: Whit Waldo * Added sample Jobs project Signed-off-by: Whit Waldo * Added documentation Signed-off-by: Whit Waldo * Added missing copyright header Signed-off-by: Whit Waldo * Downgraded Roslyn packages since master doesn't yet have the incremental source generator updates Signed-off-by: Whit Waldo * Missed a reference regarding incremental source generators Signed-off-by: Whit Waldo * Downgraded packages to fix nullability issues on build Signed-off-by: Whit Waldo * Downgraded from 8.* packages back to 6.* packages for the various Microsoft.Extensions.* packages to fix build issues Signed-off-by: Whit Waldo * Removed unnecessary assignment Signed-off-by: Whit Waldo * Added braces for clarity Signed-off-by: Whit Waldo * Added more curley braces Signed-off-by: Whit Waldo * More curly braces again Signed-off-by: Whit Waldo * Marked two properties as static Signed-off-by: Whit Waldo * Updated to handle any order of parameters to endpoint route builder delegate Signed-off-by: Whit Waldo * Updated default cancellation token value Signed-off-by: Whit Waldo * Added missing package version in Directory.Packages Signed-off-by: Whit Waldo * Fixed unit tests Signed-off-by: Whit Waldo * Added test to ensure that even if cancellation token is provided, it'll handle the mapping properly Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 94 ++-- all.sln | 24 + daprdocs/content/en/dotnet-sdk-docs/_index.md | 7 + .../en/dotnet-sdk-docs/dotnet-jobs/_index.md | 8 + .../dotnet-jobs/dotnet-jobs-howto.md | 288 ++++++++++ .../dotnet-jobs/dotnet-jobsclient-usage.md | 168 ++++++ examples/Jobs/JobsSample/JobsSample.csproj | 13 + examples/Jobs/JobsSample/Program.cs | 58 +++ .../JobsSample/Properties/launchSettings.json | 38 ++ src/Dapr.Common/AssemblyInfo.cs | 4 +- src/Dapr.Common/DaprDefaults.cs | 2 + src/Dapr.Common/DaprGenericClientBuilder.cs | 213 ++++++++ src/Dapr.Common/Extensions/EnumExtensions.cs | 27 + src/Dapr.Jobs/AssemblyInfo.cs | 16 + src/Dapr.Jobs/CronExpressionBuilder.cs | 492 ++++++++++++++++++ src/Dapr.Jobs/Dapr.Jobs.csproj | 29 ++ src/Dapr.Jobs/DaprJobsClient.cs | 99 ++++ src/Dapr.Jobs/DaprJobsClientBuilder.cs | 37 ++ src/Dapr.Jobs/DaprJobsGrpcClient.cs | 248 +++++++++ .../DaprJobsServiceCollectionExtensions.cs | 78 +++ .../Extensions/DaprSerializationExtensions.cs | 78 +++ .../EndpointRouteBuilderExtensions.cs | 95 ++++ src/Dapr.Jobs/Extensions/StringExtensions.cs | 27 + .../Extensions/TimeSpanExtensions.cs | 117 +++++ .../DaprJobScheduleConverter.cs | 41 ++ .../Iso8601DateTimeJsonConverter.cs | 61 +++ src/Dapr.Jobs/Models/DaprJobSchedule.cs | 146 ++++++ .../Models/Responses/DaprJobDetails.cs | 97 ++++ .../Extensions/EnumExtensionsTest.cs | 38 ++ .../CronExpressionBuilderTests.cs | 386 ++++++++++++++ test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj | 28 + .../DaprJobsClientBuilderTests.cs | 122 +++++ .../Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs | 172 ++++++ ...aprJobsServiceCollectionExtensionsTests.cs | 85 +++ .../EndpointRouteBuilderExtensionsTests.cs | 179 +++++++ .../Extensions/StringExtensionsTests.cs | 46 ++ .../Extensions/TimeSpanExtensionsTests.cs | 146 ++++++ .../Models/DaprJobScheduleTests.cs | 172 ++++++ .../Responses/DaprJobDetailsTests.cs | 48 ++ 39 files changed, 3981 insertions(+), 46 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md create mode 100644 examples/Jobs/JobsSample/JobsSample.csproj create mode 100644 examples/Jobs/JobsSample/Program.cs create mode 100644 examples/Jobs/JobsSample/Properties/launchSettings.json create mode 100644 src/Dapr.Common/DaprGenericClientBuilder.cs create mode 100644 src/Dapr.Common/Extensions/EnumExtensions.cs create mode 100644 src/Dapr.Jobs/AssemblyInfo.cs create mode 100644 src/Dapr.Jobs/CronExpressionBuilder.cs create mode 100644 src/Dapr.Jobs/Dapr.Jobs.csproj create mode 100644 src/Dapr.Jobs/DaprJobsClient.cs create mode 100644 src/Dapr.Jobs/DaprJobsClientBuilder.cs create mode 100644 src/Dapr.Jobs/DaprJobsGrpcClient.cs create mode 100644 src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/StringExtensions.cs create mode 100644 src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs create mode 100644 src/Dapr.Jobs/JsonConverters/DaprJobScheduleConverter.cs create mode 100644 src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs create mode 100644 src/Dapr.Jobs/Models/DaprJobSchedule.cs create mode 100644 src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs create mode 100644 test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs create mode 100644 test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs create mode 100644 test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj create mode 100644 test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs create mode 100644 test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs create mode 100644 test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs create mode 100644 test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs create mode 100644 test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs create mode 100644 test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs create mode 100644 test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs create mode 100644 test/Dapr.Jobs.Test/Responses/DaprJobDetailsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 332939a5b..4a9c47ad4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,47 +1,51 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all.sln b/all.sln index 1dd0ab3c5..6e55f247b 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{C8BB6A85-A7EA-40C0-893D-F36F317829B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{BF9828E9-5597-4D42-AA6E-6E6C12214204}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{D9697361-232F-465D-A136-4561E0E88488}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -303,6 +311,18 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.Build.0 = Release|Any CPU + {BF9828E9-5597-4D42-AA6E-6E6C12214204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF9828E9-5597-4D42-AA6E-6E6C12214204}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF9828E9-5597-4D42-AA6E-6E6C12214204}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF9828E9-5597-4D42-AA6E-6E6C12214204}.Release|Any CPU.Build.0 = Release|Any CPU + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -359,6 +379,10 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 121dde310..72e8b71d9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -69,6 +69,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria +
+
+
Jobs
+

Create and manage the scheduling and orchestration of jobs in .NET.

+ +
+
## More information diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md new file mode 100644 index 000000000..049994221 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md @@ -0,0 +1,8 @@ +--- +type: docs +title: "Dapr Jobs .NET SDK" +linkTitle: "Jobs" +weight: 50000 +description: Get up and running with Dapr Jobs and the Dapr .NET SDK +--- + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md new file mode 100644 index 000000000..c8bc66175 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -0,0 +1,288 @@ +--- +type: docs +title: "How to: Author and manage Dapr Jobs in the .NET SDK" +linkTitle: "How to: Author & manage jobs" +weight: 10000 +description: Learn how to author and manage Dapr Jobs using the .NET SDK +--- + +Let's create an endpoint that will be invoked by Dapr Jobs when it triggers, then schedule the job in the same app. We'll use the [simple example provided here](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs), for the following demonstration and walk through it as an explainer of how you can schedule one-time or recurring jobs using either an interval or Cron expression yourself. In this guide, +you will: + +- Deploy a .NET Web API application ([JobsSample](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample)) +- Utilize the .NET Jobs SDK to schedule a job invocation and set up the endpoint to be triggered + +In the .NET example project: +- The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample/Program.cs) file comprises the entirety of this demonstration. + +## 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 Jobs .NET SDK](https://github.com/dapr/dotnet-sdk) + +## Set up the environment +Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). + +```sh +git clone https://github.com/dapr/dotnet-sdk.git +``` + +From the .NET SDK root directory, navigate to the Dapr Jobs example. + +```sh +cd examples/Jobs +``` + +## Run the application locally + +To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the `JobsSample` directory. + +```sh +cd JobsSample +``` + +We'll run a command that starts both the Dapr sidecar and the .NET program at the same time. + +```sh +dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run +``` +> Dapr listens for HTTP requests at `http://localhost:3500` and internal Jobs gRPC requests at `http://localhost:4001`. +## Register the Dapr Jobs client with dependency injection +The Dapr Jobs SDK provides an extension method to simplify the registration of the Dapr Jobs client. Before completing the dependency injection registration in `Program.cs`, add the following line: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Add anywhere between these two +builder.Services.AddDaprJobsClient(); //That's it +var app = builder.Build(); +``` + +> Note that in today's implementation of the Jobs API, the app that schedules the job will also be the app that receives the trigger notification. In other words, you cannot schedule a trigger to run in another application. As a result, while you don't explicitly need the Dapr Jobs client to be registered in your application to schedule a trigger invocation endpoint, your endpoint will never be invoked without the same app also scheduling the job somehow (whether via this Dapr Jobs .NET SDK or an HTTP call to the sidecar). +It's possible that you may want to provide some configuration options to the Dapr Jobs client that +should be present with each call to the sidecar such as a Dapr API token or you want to use a non-standard +HTTP or gRPC endpoint. This is possible through an overload of the register method that allows configuration of a `DaprJobsClientBuilder` instance: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(daprJobsClientBuilder => +{ + daprJobsClientBuilder.UseDaprApiToken("abc123"); + daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint +}); + +var app = builder.Build(); +``` + +Still, it's possible that whatever values you wish to inject need to be retrieved from some other source, itself registered as a dependency. There's one more overload you can use to inject an `IServiceProvider` into the configuration action method. In the following example, we register a fictional singleton that can retrieve secrets from somewhere and pass it into the configuration method for `AddDaprJobClient` so +we can retrieve our Dapr API token from somewhere else for registration here: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => +{ + var secretRetriever = serviceProvider.GetRequiredService(); + var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value; + daprJobsClientBuilder.UseDaprApiToken(daprApiToken); + + daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); +}); + +var app = builder.Build(); +``` + +## Use the Dapr Jobs client without relying on dependency injection +While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to +deal with complicated configurations, you're not required to register the `DaprJobsClient` in this way. Rather, you can also elect to create an instance of it from a `DaprJobsClientBuilder` instance as demonstrated below: + +```cs + +public class MySampleClass +{ + public void DoSomething() + { + var daprJobsClientBuilder = new DaprJobsClientBuilder(); + var daprJobsClient = daprJobsClientBuilder.Build(); + + //Do something with the `daprJobsClient` + } +} + +``` + +## Set up a endpoint to be invoked when the job is triggered + +It's easy to set up a jobs endpoint if you're at all familiar with [minimal APIs in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) as the syntax is the same between the two. + +Once dependency injection registration has been completed, configure the application the same way you would to handle mapping an HTTP request via the minimal API functionality in ASP.NET Core. Implemented as an extension method, +pass the name of the job it should be responsive to and a delegate. Services can be injected into the delegate's arguments as you wish and you can optionally pass a `JobDetails` to get information about the job that has been triggered (e.g. access its scheduling setup or payload). + +There are two delegates you can use here. One provides an `IServiceProvider` in case you need to inject other services into the handler: + +```cs +//We have this from the example above +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +//Add our endpoint registration +app.MapDaprScheduledJob("myJob", (IServiceProvider serviceProvider, string? jobName, JobDetails? jobDetails) => { + var logger = serviceProvider.GetService(); + logger?.LogInformation("Received trigger invocation for '{jobName}'", "myJob"); + + //Do something... +}); + +app.Run(); +``` + +The other overload of the delegate doesn't require an `IServiceProvider` if not necessary: + +```cs +//We have this from the example above +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +//Add our endpoint registration +app.MapDaprScheduledJob("myJob", (string? jobName, JobDetails? jobDetails) => { + //Do something... +}); + +app.Run(); +``` + +## Register the job + +Finally, we have to register the job we want scheduled. Note that from here, all SDK methods have cancellation token support and use a default token if not otherwise set. + +There are three different ways to set up a job that vary based on how you want to configure the schedule: + +### One-time job +A one-time job is exactly that; it will run at a single point in time and will not repeat. This approach requires that you select a job name and specify a time it should be triggered. + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| scheduledTime | DateTime | The point in time when the job should be run. | Yes | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +One-time jobs can be scheduled from the Dapr Jobs client as in the following example: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken) + { + var today = DateTime.UtcNow; + var threeDaysFromNow = today.AddDays(3); + + await daprJobsClient.ScheduleOneTimeJobAsync("myJobName", threeDaysFromNow, cancellationToken: cancellationToken); + } +} +``` + +### Interval-based job +An interval-based job is one that runs on a recurring loop configured as a fixed amount of time, not unlike how [reminders](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-timers-reminders/#actor-reminders) work in the Actors building block today. These jobs can be scheduled with a number of optional arguments as well: + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| interval | TimeSpan | The interval at which the job should be triggered. | Yes | +| startingFrom | DateTime | The point in time from which the job schedule should start. | No | +| repeats | int | The maximum number of times the job should be triggered. | No | +| ttl | When the job should expires and no longer trigger. | No | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +Interval-based jobs can be scheduled from the Dapr Jobs client as in the following example: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + + public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken) + { + var hourlyInterval = TimeSpan.FromHours(1); + + //Trigger the job hourly, but a maximum of 5 times + await daprJobsClient.ScheduleIntervalJobAsync("myJobName", hourlyInterval, repeats: 5), cancellationToken: cancellationToken; + } +} +``` + +### Cron-based job +A Cron-based job is scheduled using a Cron expression. This gives more calendar-based control over when the job is triggered as it can used calendar-based values in the expression. Like the other options, these jobs can be scheduled with a number of optional arguments as well: + +| Argument Name | Type | Description | Required | +|---|---|---|---| +| jobName | string | The name of the job being scheduled. | Yes | +| cronExpression | string | The systemd Cron-like expression indicating when the job should be triggered. | Yes | +| startingFrom | DateTime | The point in time from which the job schedule should start. | No | +| repeats | int | The maximum number of times the job should be triggered. | No | +| ttl | When the job should expires and no longer trigger. | No | +| payload | ReadOnlyMemory | Job data provided to the invocation endpoint when triggered. | No | +| cancellationToken | CancellationToken | Used to cancel out of the operation early, e.g. because of an operation timeout. | No | + +A Cron-based job can be scheduled from the Dapr Jobs client as follows: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task ScheduleCronJobAsync(CancellationToken cancellationToken) + { + //At the top of every other hour on the fifth day of the month + const string cronSchedule = "0 */2 5 * *"; + + //Don't start this until next month + var now = DateTime.UtcNow; + var oneMonthFromNow = now.AddMonths(1); + var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0); + + //Trigger the job hourly, but a maximum of 5 times + await daprJobsClient.ScheduleCronJobAsync("myJobName", cronSchedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken); + } +} +``` + +## Get details of already-scheduled job +If you know the name of an already-scheduled job, you can retrieve its metadata without waiting for it to +be triggered. The returned `JobDetails` exposes a few helpful properties for consuming the information from the Dapr Jobs API: + +- If the `Schedule` property contains a Cron expression, the `IsCronExpression` property will be true and the expression will also be available in the `CronExpression` property. +- If the `Schedule` property contains a duration value, the `IsIntervalExpression` property will instead be true and the value will be converted to a `TimeSpan` value accessible from the `Interval` property. + +This can be done by using the following: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task GetJobDetailsAsync(string jobName, CancellationToken cancellationToken) + { + var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken); + return jobDetails; + } +} +``` + +## Delete a scheduled job +To delete a scheduled job, you'll need to know its name. From there, it's as simple as calling the `DeleteJobAsync` method on the Dapr Jobs client: + +```cs +public class MyOperation(DaprJobsClient daprJobsClient) +{ + public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken) + { + await daprJobsClient.DeleteJobAsync(jobName, cancellationToken); + } +} +``` \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md new file mode 100644 index 000000000..4c28e6595 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md @@ -0,0 +1,168 @@ +--- +type: docs +title: "DaprJobsClient usage" +linkTitle: "DaprJobsClient usage" +weight: 5000 +description: Essential tips and advice for using DaprJobsClient +--- + +## Lifetime management + +A `DaprJobsClient` is a version of the Dapr client that is dedicated to interacting with the Dapr Jobs API. It can be registered alongside a `DaprClient` without issue. + +It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and implements `IDisposable` to support the eager cleanup of resources. + +For best performance, create a single long-lived instance of `DaprJobsClient` and provide access to that shared instance throughout your application. `DaprJobsClient` instances are thread-safe and intended to be shared. + +Avoid creating a `DaprJobsClient` for each operation and disposing it when the operation is complete. + +## Configuring DaprJobsClient via the DaprJobsClientBuilder + +A `DaprJobsClient` can be configured by invoking methods on the `DaprJobsClientBuilder` class before calling `.Build()` to create the client itself. The settings for each `DaprJobsClient` are separate +and cannot be changed after calling `.Build()`. + +```cs +var daprJobsClient = new DaprJobsClientBuilder() + .UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars + .Build(); +``` + +The `DaprJobsClientBuilder` contains settings for: + +- The HTTP endpoint of the Dapr sidecar +- The gRPC endpoint of the Dapr sidecar +- The `JsonSerializerOptions` object used to configure JSON serialization +- The `GrpcChannelOptions` object used to configure gRPC +- The API token used to authenticate requests to the sidecar +- The factory method used to create the `HttpClient` instance used by the SDK +- The timeout used for the `HttpClient` instance when making requests to the sidecar + +The SDK will read the following environment variables to configure the default values: + +- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com` +- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com` +- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar +- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar +- `DAPR_API_TOKEN`: used to set the API token + +### Configuring gRPC channel options + +Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation). + +```cs +var daprJobsClient = new DaprJobsClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true }) + .Build(); +``` + +## Using cancellation with DaprJobsClient + +The APIs on DaprJobsClient perform asynchronous operations and accept an optional `CancellationToken` parameter. This follows a standard .NET idiom for cancellable operations. Note that when cancellation occurs, there is no guarantee that the remote endpoint stops processing the request, only that the client has stopped waiting for completion. + +When an operation is cancelled, it will throw an `OperationCancelledException`. + +## Configuring DaprJobsClient via dependency injection + +Using the built-in extension methods for registering the `DaprJobsClient` in a dependency injection container can provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` instances). + +There are three overloads available to give the developer the greatest flexibility in configuring the client for their scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure the `DaprJobsClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same instance as much as possible and avoid socket exhaution and other issues. + +In the first approach, there's no configuration done by the developer and the `DaprJobsClient` is configured with the default settings. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); //Registers the `DaprJobsClient` to be injected as needed +var app = builder.Build(); +``` + +Sometimes the developer will need to configure the created client using the various configuration options detailed above. This is done through an overload that passes in the `DaprJobsClientBuiler` and exposes methods for configuring the necessary options. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(daprJobsClientBuilder => { + //Set the API token + daprJobsClientBuilder.UseDaprApiToken("abc123"); + //Specify a non-standard HTTP endpoint + daprJobsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com"); +}); + +var app = builder.Build(); +``` + +Finally, it's possible that the developer may need to retrieve information from another service in order to populate these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the last overload: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Register a fictional service that retrieves secrets from somewhere +builder.Services.AddSingleton(); + +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => { + //Retrieve an instance of the `SecretService` from the service provider + var secretService = serviceProvider.GetRequiredService(); + var daprApiToken = secretService.GetSecret("DaprApiToken").Value; + + //Configure the `DaprJobsClientBuilder` + daprJobsClientBuilder.UseDaprApiToken(daprApiToken); +}); + +var app = builder.Build(); +``` + +## Understanding payload serialization on DaprJobsClient + +While there are many methods on the `DaprClient` that automatically serialize and deserialize data using the `System.Text.Json` serializer, this SDK takes a different philosophy. Instead, the relevant methods accept an optional payload of `ReadOnlyMemory` meaning that serialization is an exercise left to the developer and is not generally handled by the SDK. + +That said, there are some helper extension methods available for each of the scheduling methods. If you know that you want to use a type that's JSON-serializable, you can use the `Schedule*WithPayloadAsync` method for each scheduling type that accepts an `object` as a payload and an optional `JsonSerializerOptions` to use when serializing the value. This will convert the value to UTF-8 encoded bytes for you as a convenience. Here's an example of what this might look like when scheduling a Cron expression: + +```cs +public sealed record Doodad (string Name, int Value); + +//... +var doodad = new Doodad("Thing", 100); +await daprJobsClient.ScheduleCronJobWithPayloadAsync("myJob", "5 * * * *", doodad); +``` + +In the same vein, if you have a plain string value, you can use an overload of the same method to serialize a string-typed payload and the JSON serialization step will be skipped and it'll only be encoded to an array of UTF-8 encoded bytes. Here's an exampe of what this might look like when scheduling a one-time job: + +```cs +var now = DateTime.UtcNow; +var oneWeekFromNow = now.AddDays(7); +await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!"); +``` + +The `JobDetails` type returns the data as a `ReadOnlyMemory?` so the developer has the freedom to deserialize as they wish, but there are again two helper extensions included that can deserialize this to either a JSON-compatible type or a string. Both methods assume that the developer encoded the originally scheduled job (perhaps using the helper serialization methods) as these methods will not force the bytes to represent something they're not. + +To deserialize the bytes to a string, the following helper method can be used: +```cs +if (jobDetails.Payload is not null) +{ + string payloadAsString = jobDetails.Payload.DeserializeToString(); //If successful, returns a string value with the value +} +``` + +To deserialize JSON-encoded UTF-8 bytes to the corresponding type, the following helper method can be used. An overload argument is available that permits the developer to pass in their own `JsonSerializerOptions` to be applied during deserialization. + +```cs +public sealed record Doodad (string Name, int Value); + +//... +if (jobDetails.Payload is not null) +{ + var deserializedDoodad = jobDetails.Payload.DeserializeFromJsonBytes(); +} +``` + +## Error handling + +Methods on `DaprJobsClient` will throw a `DaprJobsServiceException` if an issue is encountered between the SDK and the Jobs API service running on the Dapr sidecar. If a failure is encountered because of a poorly formatted request made to the Jobs API service through this SDK, a `DaprMalformedJobException` will be thrown. In case of illegal argument values, the appropriate standard exception will be thrown (e.g. `ArgumentOutOfRangeException` or `ArgumentNullException`) with the name of the offending argument. And for anything else, a `DaprException` will be thrown. + +The most common cases of failure will be related to: + +- Incorrect argument formatting while engaging with the Jobs API +- Transient failures such as a networking problem +- Invalid data, such as a failure to deserialize a value into a type it wasn't originally serialized from + +In any of these cases, you can examine more exception details through the `.InnerException` property. diff --git a/examples/Jobs/JobsSample/JobsSample.csproj b/examples/Jobs/JobsSample/JobsSample.csproj new file mode 100644 index 000000000..4663d1d5b --- /dev/null +++ b/examples/Jobs/JobsSample/JobsSample.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs new file mode 100644 index 000000000..30ca85ba0 --- /dev/null +++ b/examples/Jobs/JobsSample/Program.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------ +// 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. +// ------------------------------------------------------------------------ + +#pragma warning disable CS0618 // Type or member is obsolete +using System.Text; +using Dapr.Jobs; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +//Set a handler to deal with incoming jobs +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); +app.MapDaprScheduledJobHandler((string? jobName, DaprJobDetails? jobDetails, ILogger? logger, CancellationToken cancellationToken) => +{ + logger?.LogInformation("Received trigger invocation for job '{jobName}'", jobName); + if (jobDetails?.Payload is not null) + { + var deserializedPayload = Encoding.UTF8.GetString(jobDetails.Payload); + logger?.LogInformation("Received invocation for the job '{jobName}' with payload '{deserializedPayload}'", + jobName, deserializedPayload); + //Do something that needs the cancellation token + } + else + { + logger?.LogWarning("Failed to deserialize payload for job '{jobName}'", jobName); + } + return Task.CompletedTask; +}, cancellationTokenSource.Token); + +app.Run(); + +await using var scope = app.Services.CreateAsyncScope(); +var logger = scope.ServiceProvider.GetRequiredService(); +var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + +logger.LogInformation("Scheduling one-time job 'myJob' to execute 10 seconds from now"); +await daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDateTime(DateTime.UtcNow.AddSeconds(10)), + Encoding.UTF8.GetBytes("This is a test")); +logger.LogInformation("Scheduled one-time job 'myJob'"); + + +#pragma warning restore CS0618 // Type or member is obsolete diff --git a/examples/Jobs/JobsSample/Properties/launchSettings.json b/examples/Jobs/JobsSample/Properties/launchSettings.json new file mode 100644 index 000000000..f45ea5b32 --- /dev/null +++ b/examples/Jobs/JobsSample/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6382", + "sslPort": 44324 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7241;http://localhost:5140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index a18d03bbc..5044876a9 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -16,8 +16,9 @@ [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.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -38,3 +39,4 @@ [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")] +[assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs index 85a4b18c8..2dd8dd378 100644 --- a/src/Dapr.Common/DaprDefaults.cs +++ b/src/Dapr.Common/DaprDefaults.cs @@ -123,7 +123,9 @@ private static string BuildEndpoint(string? endpoint, int endpointPort) //Attempt to retrieve first from the configuration var configurationValue = configuration?[name]; if (configurationValue is not null) + { return configurationValue; + } //Fall back to the environment variable with the same name or default to an empty string return Environment.GetEnvironmentVariable(name); diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs new file mode 100644 index 000000000..254953241 --- /dev/null +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -0,0 +1,213 @@ +using System.Text.Json; +using Grpc.Net.Client; +using Microsoft.Extensions.Configuration; + +namespace Dapr.Common; + +/// +/// Builder for building a generic Dapr client. +/// +public abstract class DaprGenericClientBuilder where TClientBuilder : class +{ + /// + /// Initializes a new instance of the class. + /// + protected DaprGenericClientBuilder(IConfiguration? configuration = null) + { + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + + this.GrpcChannelOptions = new GrpcChannelOptions() + { + // The gRPC client doesn't throw the right exception for cancellation + // by default, this switches that behavior on. + ThrowOperationCanceledOnCancellation = true, + }; + + this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + } + + /// + /// Property exposed for testing purposes. + /// + internal string GrpcEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal string HttpEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal Func? HttpClientFactory { get; set; } + + /// + /// Property exposed for testing purposes. + /// + public JsonSerializerOptions JsonSerializerOptions { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal GrpcChannelOptions GrpcChannelOptions { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + public string DaprApiToken { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal TimeSpan Timeout { get; private set; } + + /// + /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// The instance. + public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); + this.HttpEndpoint = httpEndpoint; + return this; + } + + /// + /// Exposed internally for testing purposes. + /// + internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) + { + this.HttpClientFactory = factory; + return this; + } + + /// + /// Overrides the legacy mechanism for building an HttpClient and uses the new + /// introduced in .NET Core 2.1. + /// + /// The factory used to create instances. + /// + public DaprGenericClientBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory) + { + this.HttpClientFactory = httpClientFactory.CreateClient; + return this; + } + + /// + /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// The instance. + public DaprGenericClientBuilder UseGrpcEndpoint(string grpcEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); + this.GrpcEndpoint = grpcEndpoint; + return this; + } + + /// + /// + /// Uses the specified when serializing or deserializing using . + /// + /// + /// The default value is created using . + /// + /// + /// Json serialization options. + /// The instance. + public DaprGenericClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + { + this.JsonSerializerOptions = options; + return this; + } + + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + /// The instance. + public DaprGenericClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + return this; + } + + /// + /// Adds the provided on every request to the Dapr runtime. + /// + /// The token to be added to the request headers/>. + /// The instance. + public DaprGenericClientBuilder UseDaprApiToken(string apiToken) + { + this.DaprApiToken = apiToken; + return this; + } + + /// + /// Sets the timeout for the HTTP client used by the Dapr client. + /// + /// + /// + public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + + /// + /// Builds out the inner DaprClient that provides the core shape of the + /// runtime gRPC client used by the consuming package. + /// + /// + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + { + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The gRPC endpoint must use http or https."); + } + + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) + { + // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The HTTP endpoint must use http or https."); + } + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + + var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + if (this.Timeout > TimeSpan.Zero) + { + httpClient.Timeout = this.Timeout; + } + + return (channel, httpClient, httpEndpoint); + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public abstract TClientBuilder Build(); +} diff --git a/src/Dapr.Common/Extensions/EnumExtensions.cs b/src/Dapr.Common/Extensions/EnumExtensions.cs new file mode 100644 index 000000000..ff9b43706 --- /dev/null +++ b/src/Dapr.Common/Extensions/EnumExtensions.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.Serialization; + +namespace Dapr.Common.Extensions; + +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.Jobs/AssemblyInfo.cs b/src/Dapr.Jobs/AssemblyInfo.cs new file mode 100644 index 000000000..870a8dde4 --- /dev/null +++ b/src/Dapr.Jobs/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// 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.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Jobs/CronExpressionBuilder.cs b/src/Dapr.Jobs/CronExpressionBuilder.cs new file mode 100644 index 000000000..4a165978d --- /dev/null +++ b/src/Dapr.Jobs/CronExpressionBuilder.cs @@ -0,0 +1,492 @@ +// ------------------------------------------------------------------------ +// 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.Serialization; +using System.Text.RegularExpressions; +using Dapr.Common.Extensions; +using ArgumentException = System.ArgumentException; +using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; + +namespace Dapr.Jobs; + +/// +/// A fluent API used to build a valid Cron expression. +/// +public sealed class CronExpressionBuilder +{ + private const string SecondsAndMinutesRegexText = @"([0-5]?\d-[0-5]?\d)|([0-5]?\d,?)|(\*(\/[0-5]?\d)?)"; + private const string HoursRegexText = @"(([0-1]?\d)|(2[0-3])-([0-1]?\d)|(2[0-3]))|(([0-1]?\d)|(2[0-3]),?)|(\*(\/([0-1]?\d)|(2[0-3]))?)"; + private const string DayOfMonthRegexText = @"\*|(\*\/(([0-2]?\d)|(3[0-1])))|(((([0-2]?\d)|(3[0-1]))(-(([0-2]?\d)|(3[0-1])))?))"; + private const string MonthRegexText = @"(^(\*\/)?((0?\d)|(1[0-2]))$)|(^\*$)|(^((0?\d)|(1[0-2]))(-((0?\d)|(1[0-2]))?)$)|(^(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*$)"; + private const string DayOfWeekRegexText = @"\*|(\*\/(0?[0-6])|(0?[0-6](-0?[0-6])?)|((,?(SUN|MON|TUE|WED|THU|FRI|SAT))+)|((SUN|MON|TUE|WED|THU|FRI|SAT)(-(SUN|MON|TUE|WED|THU|FRI|SAT))?))"; + + private static readonly Regex cronExpressionRegex = + new( + $"{SecondsAndMinutesRegexText} {SecondsAndMinutesRegexText} {HoursRegexText} {DayOfMonthRegexText} {MonthRegexText} {DayOfWeekRegexText}", RegexOptions.Compiled); + + private string seconds = "*"; + private string minutes = "*"; + private string hours = "*"; + private string dayOfMonth = "*"; + private string month = "*"; + private string dayOfWeek = "*"; + + /// + /// Reflects an expression in which the developer specifies a series of numeric values and the period they're associated + /// with indicating when the trigger should occur. + /// + /// The period of time within which the values should be associated. + /// The numerical values of the time period on which the schedule should trigger. + /// + public CronExpressionBuilder On(OnCronPeriod period, params int[] values) + { + switch (period) + { + //Validate by period + case OnCronPeriod.Second or OnCronPeriod.Minute or OnCronPeriod.Hour when values.Any(a => a is < 0 or > 59): + throw new ArgumentOutOfRangeException(nameof(values), "All values must be within 0 and 59, inclusively."); + case OnCronPeriod.DayOfMonth when values.Any(a => a is < 0 or > 31): + throw new ArgumentOutOfRangeException(nameof(values), "All values must be within 1 and 31, inclusively."); + } + + var strValue = string.Join(',', values.Distinct().OrderBy(a => a)); + + switch (period) + { + case OnCronPeriod.Second: + seconds = strValue; + break; + case OnCronPeriod.Minute: + minutes = strValue; + break; + case OnCronPeriod.Hour: + hours = strValue; + break; + case OnCronPeriod.DayOfMonth: + dayOfMonth = strValue; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the developer specifies a series of months in the year on which the trigger should occur. + /// + /// The months of the year to invoke the trigger on. + public CronExpressionBuilder On(params MonthOfYear[] months) + { + month = string.Join(',', months.Distinct().OrderBy(a => a).Select(a => a.GetValueFromEnumMember())); + return this; + } + + /// + /// Reflects an expression in which the developer specifies a series of days of the week on which the trigger should occur. + /// + /// The days of the week to invoke the trigger on. + public CronExpressionBuilder On(params DayOfWeek[] days) + { + dayOfWeek = string.Join(',', days.Distinct().OrderBy(a => a).Select(a => a.GetValueFromEnumMember())); + return this; + } + + /// + /// Reflects an expression in which the developer defines bounded range of numerical values for the specified period. + /// + /// The period of time within which the values should be associated. + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(ThroughCronPeriod period, int from, int to) + { + if (from > to) + { + throw new ArgumentException("The date representing the From property should precede the To property"); + } + + if (from == to) + { + throw new ArgumentException("The From and To properties should not be equivalent"); + } + + var stringValue = $"{from}-{to}"; + + switch (period) + { + case ThroughCronPeriod.Second: + seconds = stringValue; + break; + case ThroughCronPeriod.Minute: + minutes = stringValue; + break; + case ThroughCronPeriod.Hour: + hours = stringValue; + break; + case ThroughCronPeriod.DayOfMonth: + dayOfMonth = stringValue; + break; + case ThroughCronPeriod.Month: + month = stringValue; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the developer defines a bounded range of days. + /// + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(DayOfWeek from, DayOfWeek to) + { + if (from > to) + { + throw new ArgumentException("The day representing the From property should precede the To property"); + } + + if (from == to) + { + throw new ArgumentException("The From and To properties should not be equivalent"); + } + + dayOfWeek = $"{from.GetValueFromEnumMember()}-{to.GetValueFromEnumMember()}"; + return this; + } + + /// + /// Reflects an expression in which the developer defines a bounded range of months. + /// + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(MonthOfYear from, MonthOfYear to) + { + if (from > to) + { + throw new ArgumentException("The month representing the From property should precede the To property"); + } + + if (from == to) + { + throw new ArgumentException("The From and To properties should not be equivalent"); + } + + month = $"{from.GetValueFromEnumMember()}-{to.GetValueFromEnumMember()}"; + return this; + } + + /// + /// Reflects an expression in which the trigger should happen each time the value of the specified period changes. + /// + /// The period of time that should be evaluated. + /// + public CronExpressionBuilder Each(CronPeriod period) + { + switch (period) + { + case CronPeriod.Second: + seconds = "*"; + break; + case CronPeriod.Minute: + minutes = "*"; + break; + case CronPeriod.Hour: + hours = "*"; + break; + case CronPeriod.DayOfMonth: + dayOfMonth = "*"; + break; + case CronPeriod.Month: + month = "*"; + break; + case CronPeriod.DayOfWeek: + dayOfWeek = "*"; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the trigger should happen at a regular interval of the specified period type. + /// + /// The length of time represented in a unit interval. + /// The number of period units that should elapse between each trigger. + /// + public CronExpressionBuilder Every(EveryCronPeriod period, int interval) + { + if (interval < 0) + { + throw new ArgumentOutOfRangeException(nameof(interval)); + } + + var value = $"*/{interval}"; + + switch (period) + { + case EveryCronPeriod.Second: + seconds = value; + break; + case EveryCronPeriod.Minute: + minutes = value; + break; + case EveryCronPeriod.Hour: + hours = value; + break; + case EveryCronPeriod.Month: + month = value; + break; + case EveryCronPeriod.DayInMonth: + dayOfMonth = value; + break; + case EveryCronPeriod.DayInWeek: + dayOfWeek = value; + break; + } + + return this; + } + + /// + /// Validates whether a given expression is valid Cron syntax. + /// + /// The string to evaluate. + /// True if the expression is valid Cron syntax; false if not. + internal static bool IsCronExpression(string expression) => expression.Split(' ').Length == 6 && cronExpressionRegex.IsMatch(expression); + + /// + /// Builds the Cron expression. + /// + /// + public override string ToString() => $"{seconds} {minutes} {hours} {dayOfMonth} {month} {dayOfWeek}"; +} + +/// +/// Identifies the valid Cron periods in an "On" expression. +/// +public enum OnCronPeriod +{ + /// + /// Identifies the second value for an "On" expression. + /// + Second, + /// + /// Identifies the minute value for an "On" expression. + /// + Minute, + /// + /// Identifies the hour value for an "On" expression. + /// + Hour, + /// + /// Identifies the day in the month for an "On" expression. + /// + DayOfMonth +} + +/// +/// Identifies the valid Cron periods in an "Every" expression. +/// +public enum EveryCronPeriod +{ + /// + /// Identifies the second value in an "Every" expression. + /// + Second, + /// + /// Identifies the minute value in an "Every" expression. + /// + Minute, + /// + /// Identifies the hour value in an "Every" expression. + /// + Hour, + /// + /// Identifies the month value in an "Every" expression. + /// + Month, + /// + /// Identifies the days in the month value in an "Every" expression. + /// + DayInMonth, + /// + /// Identifies the days in the week value in an "Every" expression. + /// + DayInWeek, +} + +/// +/// Identifies the various Cron periods valid to use in a "Through" expression. +/// +public enum ThroughCronPeriod +{ + /// + /// Identifies the second value in the Cron expression. + /// + Second, + /// + /// Identifies the minute value in the Cron expression. + /// + Minute, + /// + /// Identifies the hour value in the Cron expression. + /// + Hour, + /// + /// Identifies the day of month value in the Cron expression. + /// + DayOfMonth, + /// + /// Identifies the month value in the Cron expression. + /// + Month +} + +/// +/// Identifies the various Cron periods. +/// +public enum CronPeriod +{ + /// + /// Identifies the second value in the Cron expression. + /// + Second, + /// + /// Identifies the minute value in the Cron expression. + /// + Minute, + /// + /// Identifies the hour value in the Cron expression. + /// + Hour, + /// + /// Identifies the day of month value in the Cron expression. + /// + DayOfMonth, + /// + /// Identifies the month value in the Cron expression. + /// + Month, + /// + /// Identifies the day of week value in the Cron expression. + /// + DayOfWeek +} + +/// +/// Identifies the days in the week. +/// +public enum DayOfWeek +{ + /// + /// Sunday. + /// + [EnumMember(Value = "SUN")] + Sunday = 0, + /// + /// Monday. + /// + [EnumMember(Value = "MON")] + Monday = 1, + /// + /// Tuesday. + /// + [EnumMember(Value = "TUE")] + Tuesday = 2, + /// + /// Wednesday. + /// + [EnumMember(Value = "WED")] + Wednesday = 3, + /// + /// Thursday. + /// + [EnumMember(Value = "THU")] + Thursday = 4, + /// + /// Friday. + /// + [EnumMember(Value = "FRI")] + Friday = 5, + /// + /// Saturday. + /// + [EnumMember(Value = "SAT")] + Saturday = 6 +} + +/// +/// Identifies the months in the year. +/// +public enum MonthOfYear +{ + /// + /// Month of January. + /// + [EnumMember(Value = "JAN")] + January = 1, + /// + /// Month of February. + /// + [EnumMember(Value = "FEB")] + February = 2, + /// + /// Month of March. + /// + [EnumMember(Value = "MAR")] + March = 3, + /// + /// Month of April. + /// + [EnumMember(Value = "APR")] + April = 4, + /// + /// Month of May. + /// + [EnumMember(Value = "MAY")] + May = 5, + /// + /// Month of June. + /// + [EnumMember(Value = "JUN")] + June = 6, + /// + /// Month of July. + /// + [EnumMember(Value = "JUL")] + July = 7, + /// + /// Month of August. + /// + [EnumMember(Value = "AUG")] + August = 8, + /// + /// Month of September. + /// + [EnumMember(Value = "SEP")] + September = 9, + /// + /// Month of October. + /// + [EnumMember(Value = "OCT")] + October = 10, + /// + /// Month of November. + /// + [EnumMember(Value = "NOV")] + November = 11, + /// + /// Month of December. + /// + [EnumMember(Value = "DEC")] + December = 12 +} diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj new file mode 100644 index 000000000..74c9bec23 --- /dev/null +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -0,0 +1,29 @@ + + + + net6;net8 + enable + enable + Dapr.Jobs + Dapr Jobs Authoring SDK + Dapr Jobs SDK for scheduling jobs and tasks with Dapr + alpha + + + + + + + + + + + + + + + + + + + diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs new file mode 100644 index 000000000..4dd4abd70 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; + +namespace Dapr.Jobs; + +/// +/// +/// Defines client operations for managing Dapr jobs. +/// Use to create a or register +/// for use with dependency injection via +/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient. +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance +/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid +/// creating a disposing a client instance for each operation that the application performs - this can lead to socket +/// exhaustion and other problems. +/// +/// +public abstract class DaprJobsClient : IDisposable +{ + private bool disposed; + + /// + /// Schedules a job with Dapr. + /// + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, + ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, + DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default); + + /// + /// Retrieves the details of a registered job. + /// + /// The jobName of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task GetJobAsync(string jobName, CancellationToken cancellationToken = default); + + /// + /// Deletes the specified job. + /// + /// The jobName of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default); + + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) + { + return null; + } + + return new KeyValuePair("dapr-api-token", apiToken); + } + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs new file mode 100644 index 000000000..390d52236 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Jobs; + +/// +/// Builds a . +/// +public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + public override DaprJobsClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(); + + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; + + return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader); + } +} diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs new file mode 100644 index 000000000..f23ef67fd --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -0,0 +1,248 @@ +// ------------------------------------------------------------------------ +// 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.Net.Http.Headers; +using System.Reflection; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Jobs; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal sealed class DaprJobsGrpcClient : DaprJobsClient +{ + /// + /// Present only for testing purposes. + /// + internal readonly HttpClient httpClient; + + /// + /// Used to populate options headers with API token value. + /// + internal readonly KeyValuePair? apiTokenHeader; + + private readonly Autogenerated.Dapr.DaprClient client; + private readonly string userAgent = UserAgent().ToString(); + + // property exposed for testing purposes + internal Autogenerated.Dapr.DaprClient Client => client; + + internal DaprJobsGrpcClient( + Autogenerated.Dapr.DaprClient innerClient, + HttpClient httpClient, + KeyValuePair? apiTokenHeader) + { + this.client = innerClient; + this.httpClient = httpClient; + this.apiTokenHeader = apiTokenHeader; + } + + /// + /// Schedules a job with Dapr. + /// + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, + ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, + DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(jobName, nameof(jobName)); + ArgumentNullException.ThrowIfNull(schedule, nameof(schedule)); + + var job = new Autogenerated.Job { Name = jobName, Schedule = schedule.ExpressionValue }; + + if (startingFrom is not null) + { + job.DueTime = ((DateTimeOffset)startingFrom).ToString("O"); + } + + if (repeats is not null) + { + if (repeats < 0) + { + throw new ArgumentOutOfRangeException(nameof(repeats)); + } + + job.Repeats = (uint)repeats; + } + + if (payload is not null) + { + job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; + } + + if (ttl is not null) + { + if (ttl <= startingFrom) + { + throw new ArgumentException( + $"When both {nameof(ttl)} and {nameof(startingFrom)} are specified, {nameof(ttl)} must represent a later point in time"); + } + + job.Ttl = ((DateTimeOffset)ttl).ToString("O"); + } + + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.ScheduleJobAlpha1Async(envelope, callOptions); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Schedule job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Retrieves the details of a registered job. + /// + /// The name of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task GetJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + { + throw new ArgumentNullException(nameof(jobName)); + } + + try + { + var envelope = new Autogenerated.GetJobRequest { Name = jobName }; + var callOptions = CreateCallOptions(headers: null, cancellationToken); + var response = await client.GetJobAlpha1Async(envelope, callOptions); + return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule)) + { + DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, + Ttl = response.Job.Ttl is not null ? DateTime.Parse(response.Job.Ttl) : null, + RepeatCount = response.Job.Repeats == default ? null : (int?)response.Job.Repeats, + Payload = response.Job.Data.ToByteArray() + }; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Get job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + throw new DaprException("Get job operation failed: the Dapr endpoint did not return the expected value."); + } + + /// + /// Deletes the specified job. + /// + /// The name of the job. + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + { + throw new ArgumentNullException(nameof(jobName)); + } + + try + { + var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; + var callOptions = CreateCallOptions(headers: null, cancellationToken); + await client.DeleteJobAlpha1Async(envelope, callOptions); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Delete job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.httpClient.Dispose(); + } + } + + private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) + { + var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + + callOptions.Headers!.Add("User-Agent", this.userAgent); + + if (apiTokenHeader is not null) + { + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + + return callOptions; + } + + /// + /// Returns the value for the User-Agent. + /// + /// A containing the value to use for the User-Agent. + private static ProductInfoHeaderValue UserAgent() + { + var assembly = typeof(DaprJobsClient).Assembly; + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } +} diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs new file mode 100644 index 000000000..67e718985 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Jobs.Extensions; + +/// +/// Contains extension methods for using Dapr Jobs with dependency injection. +/// +public static class DaprJobsServiceCollectionExtensions +{ + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the . + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(builder); + + return builder.Build(); + }); + + return serviceCollection; + } + + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + + return serviceCollection; + } +} diff --git a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs new file mode 100644 index 000000000..1f02a32cd --- /dev/null +++ b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; +using System.Text.Json; +using Dapr.Jobs.Models; + +namespace Dapr.Jobs.Extensions; + +/// +/// Provides helper extensions for performing serialization operations when scheduling one-time Cron jobs for the developer. +/// +public static class DaprJobsSerializationExtensions +{ + /// + /// Default JSON serializer options. + /// + private static readonly JsonSerializerOptions defaultOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + /// + /// Schedules a job with Dapr. + /// + /// The instance. + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job expressed as a JSON-serializable object. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Optional JSON serialization options. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, + object payload, DateTime? startingFrom = null, int? repeats = null, JsonSerializerOptions? jsonSerializerOptions = null, DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var serializerOptions = jsonSerializerOptions ?? defaultOptions; + var payloadBytes = + JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); + + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + } + + /// + /// Schedules a job with Dapr. + /// + /// The instance. + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job expressed as a string. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, + string payload, DateTime? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var payloadBytes = Encoding.UTF8.GetBytes(payload); + + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + } +} diff --git a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..26ef579cc --- /dev/null +++ b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using Dapr.Jobs.Models.Responses; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Dapr.Jobs.Extensions; + +/// +/// Provides extension methods to register endpoints for Dapr Job Scheduler invocations. +/// +public static class EndpointRouteBuilderExtensions +{ + /// + /// Provides for a handler to be provided that allows the user to dictate how various jobs should be handled without + /// necessarily knowing the name of the job at build time. + /// + /// The to add the route to. + /// The asynchronous action provided by the developer that handles any inbound requests. The first two + /// parameters must be a nullable for the jobName and a nullable with the + /// payload details, but otherwise can be populated with additional services to be injected into the delegate. + /// Cancellation token that will be passed in as the last parameter to the delegate action. + public static IEndpointRouteBuilder MapDaprScheduledJobHandler(this IEndpointRouteBuilder endpoints, + Delegate action, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(endpoints, nameof(endpoints)); + ArgumentNullException.ThrowIfNull(action, nameof(action)); + + endpoints.MapPost("/job/{jobName}", async context => + { + var jobName = (string?)context.Request.RouteValues["jobName"]; + DaprJobDetails? jobPayload = null; + + if (context.Request.ContentLength is > 0) + { + using var reader = new StreamReader(context.Request.Body); + var body = await reader.ReadToEndAsync(); + + try + { + var deserializedJobPayload = JsonSerializer.Deserialize(body); + jobPayload = deserializedJobPayload?.ToType() ?? null; + } + catch (JsonException) + { + jobPayload = null; + } + } + + var parameters = new Dictionary + { + { typeof(string), jobName }, + { typeof(DaprJobDetails), jobPayload }, + { typeof(CancellationToken), CancellationToken.None } + }; + + var actionParameters = action.Method.GetParameters(); + var invokeParameters = new object?[actionParameters.Length]; + + for (var a = 0; a < actionParameters.Length; a++) + { + var parameterType = actionParameters[a].ParameterType; + + if (parameters.TryGetValue(parameterType, out var value)) + { + invokeParameters[a] = value; + } + else + { + invokeParameters[a] = context.RequestServices.GetService(parameterType); + } + } + + var result = action.DynamicInvoke(invokeParameters.ToArray()); + if (result is Task task) + { + await task; + } + }); + + return endpoints; + } +} diff --git a/src/Dapr.Jobs/Extensions/StringExtensions.cs b/src/Dapr.Jobs/Extensions/StringExtensions.cs new file mode 100644 index 000000000..98e3525de --- /dev/null +++ b/src/Dapr.Jobs/Extensions/StringExtensions.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs.Extensions; + +internal static class StringExtensions +{ + /// + /// Extension method that validates a string against a list of possible matches. + /// + /// The string value to evaluate. + /// The possible values to look for a match within. + /// The type of string comparison to perform. + /// True if the value ends with any of the possible values; otherwise false. + public static bool EndsWithAny(this string value, IReadOnlyList possibleValues, + StringComparison comparisonType) => possibleValues.Any(val => value.EndsWith(val, comparisonType)); +} diff --git a/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs b/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs new file mode 100644 index 000000000..2c6b1af98 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs @@ -0,0 +1,117 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; +using System.Text.RegularExpressions; + +namespace Dapr.Jobs; + +/// +/// Provides extension methods used with . +/// +internal static class TimeSpanExtensions +{ + private static readonly Regex hourRegex = new Regex(@"(\d+)h", RegexOptions.Compiled); + private static readonly Regex minuteRegex = new Regex(@"(\d+)m", RegexOptions.Compiled); + private static readonly Regex secondRegex = new Regex(@"(\d+)s", RegexOptions.Compiled); + private static readonly Regex millisecondRegex = new Regex(@"(\d+)q", RegexOptions.Compiled); + + /// + /// Creates a duration string that matches the specification at https://pkg.go.dev/time#ParseDuration per the + /// Jobs API specification https://v1-14.docs.dapr.io/reference/api/jobs_api/#schedule-a-job. + /// + /// The timespan being evaluated. + /// + public static string ToDurationString(this TimeSpan timespan) + { + var sb = new StringBuilder(); + + //Hours is the largest unit of measure in the duration string + if (timespan.Hours > 0) + { + sb.Append($"{timespan.Hours}h"); + } + + if (timespan.Minutes > 0) + { + sb.Append($"{timespan.Minutes}m"); + } + + if (timespan.Seconds > 0) + { + sb.Append($"{timespan.Seconds}s"); + } + + if (timespan.Milliseconds > 0) + { + sb.Append($"{timespan.Milliseconds}ms"); + } + + return sb.ToString(); + } + + /// + /// Validates whether a given string represents a parseable Golang duration string. + /// + /// The duration string to parse. + /// True if the string represents a parseable interval duration; false if not. + public static bool IsDurationString(this string interval) + { + interval = interval.Replace("ms", "q"); + return hourRegex.Match(interval).Success || + minuteRegex.Match(interval).Success || + secondRegex.Match(interval).Success || + millisecondRegex.Match(interval).Success; + } + + /// + /// Creates a given a Golang duration string. + /// + /// The duration string to parse. + /// A timespan value. + public static TimeSpan FromDurationString(this string interval) + { + interval = interval.Replace("ms", "q"); + + int hours = 0; + int minutes = 0; + int seconds = 0; + int milliseconds = 0; + + var hourMatch = hourRegex.Match(interval); + if (hourMatch.Success) + { + hours = int.Parse(hourMatch.Groups[1].Value); + } + + var minuteMatch = minuteRegex.Match(interval); + if (minuteMatch.Success) + { + minutes = int.Parse(minuteMatch.Groups[1].Value); + } + + var secondMatch = secondRegex.Match(interval); + if (secondMatch.Success) + { + seconds = int.Parse(secondMatch.Groups[1].Value); + } + + var millisecondMatch = millisecondRegex.Match(interval); + if (millisecondMatch.Success) + { + milliseconds = int.Parse(millisecondMatch.Groups[1].Value); + } + + return new TimeSpan(0, hours, minutes, seconds, milliseconds); + } +} diff --git a/src/Dapr.Jobs/JsonConverters/DaprJobScheduleConverter.cs b/src/Dapr.Jobs/JsonConverters/DaprJobScheduleConverter.cs new file mode 100644 index 000000000..b42051469 --- /dev/null +++ b/src/Dapr.Jobs/JsonConverters/DaprJobScheduleConverter.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Jobs.Models; + +namespace Dapr.Jobs.JsonConverters; + +internal sealed class DaprJobScheduleConverter : JsonConverter +{ + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override DaprJobSchedule? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var exprValue = reader.GetString(); + return exprValue is null ? null : DaprJobSchedule.FromExpression(exprValue); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, DaprJobSchedule value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ExpressionValue); + } +} diff --git a/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs b/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs new file mode 100644 index 000000000..90ffd3d4d --- /dev/null +++ b/src/Dapr.Jobs/JsonConverters/Iso8601DateTimeJsonConverter.cs @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using System.Text.Json; + +namespace Dapr.Jobs.JsonConverters; + +/// +/// Converts from an ISO 8601 DateTime to a string and back. This is primarily used to serialize +/// dates for use with CosmosDB. +/// +public sealed class Iso8601DateTimeJsonConverter : JsonConverter +{ + /// Reads and converts the JSON to a . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + var dateString = reader.GetString(); + if (DateTimeOffset.TryParse(dateString, out var dateTimeOffset)) + { + return dateTimeOffset; + } + + throw new JsonException($"Unable to convert \"{dateString}\" to {nameof(DateTimeOffset)}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) + { + if (value is not null) + { + writer.WriteStringValue(value.Value.ToString("O")); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/src/Dapr.Jobs/Models/DaprJobSchedule.cs b/src/Dapr.Jobs/Models/DaprJobSchedule.cs new file mode 100644 index 000000000..c1b592e12 --- /dev/null +++ b/src/Dapr.Jobs/Models/DaprJobSchedule.cs @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.JsonConverters; + +namespace Dapr.Jobs.Models; + +/// +/// Used to build a schedule for a job. +/// +[JsonConverter(typeof(DaprJobScheduleConverter))] +public sealed class DaprJobSchedule +{ + /// + /// A regular expression used to evaluate whether a given prefix period embodies an @every statement. + /// + private static readonly Regex isEveryExpression = new(@"^@every (\d+(m?s|m|h))+$", RegexOptions.Compiled); + /// + /// The various prefixed period values allowed. + /// + private static readonly string[] acceptablePeriodValues = { "yearly", "monthly", "weekly", "daily", "midnight", "hourly" }; + + /// + /// The value of the expression represented by the schedule. + /// + public string ExpressionValue { get; } + + /// + /// Initializes the value of based on the provided value from each of the factory methods. + /// + /// + /// Developers are intended to create a new using the provided static factory methods. + /// + /// The value of the scheduling expression. + internal DaprJobSchedule(string expressionValue) + { + ExpressionValue = expressionValue; + } + + /// + /// Specifies a schedule built using the fluent Cron expression builder. + /// + /// The fluent Cron expression builder. + public static DaprJobSchedule FromCronExpression(CronExpressionBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + return new DaprJobSchedule(builder.ToString()); + } + + /// + /// Specifies a single point in time. + /// + /// The date and time when the job should be triggered. + /// + public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) + { + ArgumentNullException.ThrowIfNull(scheduledTime, nameof(scheduledTime)); + return new DaprJobSchedule(scheduledTime.ToString("O")); + } + + /// + /// Specifies a schedule using a Cron-like expression or '@' prefixed period strings. + /// + /// The systemd Cron-like expression indicating when the job should be triggered. + public static DaprJobSchedule FromExpression(string expression) + { + ArgumentNullException.ThrowIfNull(expression, nameof(expression)); + return new DaprJobSchedule(expression); + } + + /// + /// Specifies a schedule using a duration interval articulated via a . + /// + /// The duration interval. + public static DaprJobSchedule FromDuration(TimeSpan duration) + { + ArgumentNullException.ThrowIfNull(duration, nameof(duration)); + return new DaprJobSchedule(duration.ToDurationString()); + } + + /// + /// Specifies a schedule in which the job is triggered to run once a year. + /// + public static DaprJobSchedule Yearly { get; } = new DaprJobSchedule("@yearly"); + + /// + /// Specifies a schedule in which the job is triggered monthly. + /// + public static DaprJobSchedule Monthly { get; } = new DaprJobSchedule("@monthly"); + + /// + /// Specifies a schedule in which the job is triggered weekly. + /// + public static DaprJobSchedule Weekly { get; } =new DaprJobSchedule("@weekly"); + + /// + /// Specifies a schedule in which the job is triggered daily. + /// + public static DaprJobSchedule Daily { get; } = new DaprJobSchedule("@daily"); + + /// + /// Specifies a schedule in which the job is triggered once a day at midnight. + /// + public static DaprJobSchedule Midnight { get; } = new DaprJobSchedule("@midnight"); + + /// + /// Specifies a schedule in which the job is triggered at the top of every hour. + /// + public static DaprJobSchedule Hourly { get; } = new DaprJobSchedule("@hourly"); + + /// + /// Reflects that the schedule represents a prefixed period expression. + /// + public bool IsPrefixedPeriodExpression => + ExpressionValue.StartsWith('@') && + (isEveryExpression.IsMatch(ExpressionValue) || + ExpressionValue.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture)); + + /// + /// Reflects that the schedule represents a fixed point in time. + /// + public bool IsPointInTimeExpression => DateTimeOffset.TryParse(ExpressionValue, out _); + + /// + /// Reflects that the schedule represents a Golang duration expression. + /// + public bool IsDurationExpression => ExpressionValue.IsDurationString(); + + /// + /// Reflects that the schedule represents a Cron expression. + /// + public bool IsCronExpression => CronExpressionBuilder.IsCronExpression(ExpressionValue); +} diff --git a/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs new file mode 100644 index 000000000..9b940beed --- /dev/null +++ b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using Dapr.Jobs.JsonConverters; + +namespace Dapr.Jobs.Models.Responses; + +/// +/// Represents the details of a retrieved job. +/// +/// The job schedule. +public sealed record DaprJobDetails(DaprJobSchedule Schedule) +{ + /// + /// Allows for jobs with fixed repeat counts. + /// + public int? RepeatCount { get; init; } = null; + + /// + /// Identifies a point-in-time representing when the job schedule should start from, + /// or as a "one-shot" time if other scheduling fields are not provided. + /// + public DateTimeOffset? DueTime { get; init; } = null; + + /// + /// A point-in-time value representing with the job should expire. + /// + /// + /// This must be greater than if both are set. + /// + public DateTimeOffset? Ttl { get; init; } = null; + + /// + /// Stores the main payload of the job which is passed to the trigger function. + /// + public byte[]? Payload { get; init; } = null; +} + +/// +/// A deserializable version of the . +/// +internal sealed record DeserializableDaprJobDetails +{ + /// + /// Represents the schedule that triggers the job. + /// + public string? Schedule { get; init; } + + /// + /// Allows for jobs with fixed repeat counts. + /// + public int? RepeatCount { get; init; } = null; + + /// + /// Identifies a point-in-time representing when the job schedule should start from, + /// or as a "one-shot" time if other scheduling fields are not provided. + /// + [JsonConverter(typeof(Iso8601DateTimeJsonConverter))] + public DateTimeOffset? DueTime { get; init; } = null; + + /// + /// A point-in-time value representing with the job should expire. + /// + /// + /// This must be greater than if both are set. + /// + [JsonConverter(typeof(Iso8601DateTimeJsonConverter))] + public DateTimeOffset? Ttl { get; init; } = null; + + /// + /// Stores the main payload of the job which is passed to the trigger function. + /// + public byte[]? Payload { get; init; } = null; + + public DaprJobDetails ToType() + { + var schedule = DaprJobSchedule.FromExpression(Schedule ?? string.Empty); + return new DaprJobDetails(schedule) + { + DueTime = DueTime, + Payload = Payload, + RepeatCount = RepeatCount, + Ttl = Ttl + }; + } +} diff --git a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs new file mode 100644 index 000000000..84e2998d6 --- /dev/null +++ b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using Dapr.Common.Extensions; +using Xunit; + +namespace Dapr.Common.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.Jobs.Test/CronExpressionBuilderTests.cs b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs new file mode 100644 index 000000000..38031e0eb --- /dev/null +++ b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs @@ -0,0 +1,386 @@ +// ------------------------------------------------------------------------ +// 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; +using ArgumentException = System.ArgumentException; + +namespace Dapr.Jobs.Test; + +public sealed class CronExpressionBuilderTests +{ + [Fact] + public void WildcardByDefault() + { + var builder = new CronExpressionBuilder(); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void WildcardByAssertion() + { + var builder = new CronExpressionBuilder() + .Each(CronPeriod.Second) + .Each(CronPeriod.Minute) + .Each(CronPeriod.Hour) + .Each(CronPeriod.DayOfWeek) + .Each(CronPeriod.DayOfMonth) + .Each(CronPeriod.Month); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void OnVariations() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 5) + .On(OnCronPeriod.Minute, 12) + .On(OnCronPeriod.Hour, 16) + .On(OnCronPeriod.DayOfMonth, 7); + var result = builder.ToString(); + Assert.Equal("5 12 16 7 * *", result); + } + + [Fact] + public void BottomOfEveryMinute() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 30); + var result = builder.ToString(); + Assert.Equal("30 * * * * *", result); + } + + [Fact] + public void EveryFiveSeconds() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Second, 5); + var result = builder.ToString(); + Assert.Equal("*/5 * * * * *", result); + } + + [Fact] + public void BottomOfEveryHour() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 30); + var result = builder.ToString(); + Assert.Equal("0 30 * * * *", result); + } + + [Fact] + public void EveryTwelveMinutes() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Minute, 12); + var result = builder.ToString(); + Assert.Equal("* */12 * * * *", result); + } + + [Fact] + public void EveryHour() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0); + var result = builder.ToString(); + Assert.Equal("0 0 * * * *", result); + } + + [Fact] + public void EveryFourHours() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Hour, 4); + var result = builder.ToString(); + Assert.Equal("* * */4 * * *", result); + } + + [Fact] + public void EveryOtherMonth() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Month, 2); + var result = builder.ToString(); + Assert.Equal("* * * * */2 *", result); + } + + [Fact] + public void EachMonth() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Month, 4) + .Each(CronPeriod.Month); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void EveryDayAtMidnight() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 0); + var result = builder.ToString(); + Assert.Equal("0 0 0 * * *", result); + } + + [Fact] + public void EveryFourthDayInJanAprAugAndDecIfTheDayIsWednesdayOrFriday() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 30) + .On(OnCronPeriod.Minute, 15) + .On(OnCronPeriod.Hour, 6) + .Every(EveryCronPeriod.DayInMonth, 4) + .On(MonthOfYear.January, MonthOfYear.April, MonthOfYear.August, MonthOfYear.December) + .On(DayOfWeek.Wednesday, DayOfWeek.Friday); + var result = builder.ToString(); + Assert.Equal("30 15 6 */4 JAN,APR,AUG,DEC WED,FRI", result); + } + + [Fact] + public void EveryValidation() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Second, 10) + .Every(EveryCronPeriod.Minute, 8) + .Every(EveryCronPeriod.Hour, 2) + .Every(EveryCronPeriod.DayInMonth, 5) + .Every(EveryCronPeriod.DayInWeek, 2) + .Every(EveryCronPeriod.Month, 3); + var result = builder.ToString(); + Assert.Equal("*/10 */8 */2 */5 */3 */2", result); + } + + [Fact] + public void EveryDayAtNoon() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 12); + var result = builder.ToString(); + Assert.Equal("0 0 12 * * *", result); + } + + [Fact] + public void MidnightOnTuesdaysAndFridays() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 0) + .On(DayOfWeek.Tuesday, DayOfWeek.Friday); + var result = builder.ToString(); + Assert.Equal("0 0 0 * * TUE,FRI", result); + } + + [Fact] + public void FourThirtyPmOnWednesdayThroughSaturdayFromOctoberToDecember() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 30) + .On(OnCronPeriod.Hour, 16) + .Through(DayOfWeek.Wednesday, DayOfWeek.Saturday) + .Through(MonthOfYear.October, MonthOfYear.December); + var result = builder.ToString(); + Assert.Equal("0 30 16 * OCT-DEC WED-SAT", result); + } + + [Fact] + public void ThroughFirstAvailableUnits() + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Second, 0, 15) + .Through(ThroughCronPeriod.Minute, 0, 15) + .Through(ThroughCronPeriod.Hour, 0, 15) + .Through(ThroughCronPeriod.DayOfMonth, 1, 10) + .Through(ThroughCronPeriod.Month, 0, 8); + var result = builder.ToString(); + Assert.Equal("0-15 0-15 0-15 1-10 0-8 *", result); + } + + [Fact] + public void ShouldThrowIfIntervalIsBelowRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Minute, -5); + }); + } + + [Fact] + public void ShouldThrowIfRangeValuesAreEqual() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Hour, 8, 8); + }); + } + + [Fact] + public void ShouldThrowIfRangeValuesAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(MonthOfYear.December, MonthOfYear.February); + }); + } + + [Fact] + public void ShouldThrowIfRangedMonthsAreEqual() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(MonthOfYear.April, MonthOfYear.April); + }); + } + + [Fact] + public void ShouldThrowIfRangedMonthsAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Minute, 10, 5); + }); + } + + [Fact] + public void ShouldThrowIfRangedDaysAreEqualInRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(DayOfWeek.Thursday, DayOfWeek.Thursday); + }); + } + + [Fact] + public void ShouldThrowIfRangedDaysAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(DayOfWeek.Thursday, DayOfWeek.Monday); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, -2); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange2() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Hour, -10); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange3() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.DayOfMonth, -5); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreAboveRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Minute, 60); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreAboveRange2() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.DayOfMonth, 32); + }); + } + + [Theory] + [InlineData("* * * * *", false)] + [InlineData("* * * * * *", true)] + [InlineData("5 12 16 7 * *", true)] + [InlineData("30 * * * * *", true)] + [InlineData("*/5 * * * * *", true)] + [InlineData("0 30 * * * *", true)] + [InlineData("* */12 * * * *", true)] + [InlineData("0 0 * * * *", true)] + [InlineData("* * */4 * * *", true)] + [InlineData("* * * * */2 *", true)] + [InlineData("0 0 0 * * *", true)] + [InlineData("30 15 6 */4 JAN,APR,AUG WED,FRI", true)] + [InlineData("*/10 */8 */2 */5 */3 *", true)] + [InlineData("0 0 12 * * *", true)] + [InlineData("0 0 0 * * TUE,FRI", true)] + [InlineData("0 0 0 * * TUE", true)] + [InlineData("0 0 0 * * TUE-FRI", true)] + [InlineData("0 30 16 * OCT SAT", true)] + [InlineData("0 30 16 * OCT,DEC WED,SAT", true)] + [InlineData("0 30 16 * OCT-DEC WED-SAT", true)] + [InlineData("0-15 * * * * *", true)] + [InlineData("0-15 02-59 * * * *", true)] + [InlineData("0-15 02-59 07-23 * * *", true)] + [InlineData("0-15 0-15 0-15 1-10 8-16 *", true)] + [InlineData("5 12 16 7 FEB *", true)] + [InlineData("5 12 16 7 * MON", true)] + [InlineData("5 12 16 7 JAN SAT", true)] + [InlineData("5 * * * FEB SUN", true)] + [InlineData("* * */2 * * *", true)] + [InlineData("* * * */5 * *", true)] + [InlineData("0,01,3 0,01,2 0,01,2 00,1,02 JAN,FEB,MAR,APR SUN,MON,TUE,WED", true)] + [InlineData("* * * * JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC SUN,MON,TUE,WED,THU,FRI,SAT", true)] + [InlineData("30 15 6 */4 JAN,APR,AUG WED-FRI", true)] + [InlineData("*/10 */8 */2 */5 */3 */2", true)] + [InlineData("0 0 0 * OCT SAT", true)] + [InlineData("0 0 0 * OCT,DEC WED,SAT", true)] + [InlineData("0 0 0 * OCT-DEC WED-SAT", true)] + [InlineData("1-14 2-59 20-23 * * *", true)] + [InlineData("00-59 0-59 00-23 1-31 JAN-DEC SUN-SAT", true)] + [InlineData("0-59 0-59 0-23 1-31 1-12 0-6", true)] + [InlineData("*/1 2,4,5 * 2-9 JAN,FEB,DEC MON-WED", true)] + public void ValidateCronExpression(string cronValue, bool isValid) + { + var result = CronExpressionBuilder.IsCronExpression(cronValue); + Assert.Equal(result, isValid); + } +} diff --git a/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj new file mode 100644 index 000000000..0e0c9017f --- /dev/null +++ b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj @@ -0,0 +1,28 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs new file mode 100644 index 000000000..bdfa2d8d2 --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTests.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Text.Json; +using Grpc.Net.Client; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class DaprJobsClientBuilderTest +{ + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() + { + DaprJobsClientBuilder builder = new DaprJobsClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprJobsClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesHttpEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_SetsApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_SetsNullApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken(null); + builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenSet_SetsApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.Equal(default, entry); + } + + [Fact] + public void DaprJobsClientBuilder_SetsTimeout() + { + var builder = new DaprJobsClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} diff --git a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs new file mode 100644 index 000000000..4f6168830 --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs @@ -0,0 +1,172 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Net.Http; +using Dapr.Jobs.Models; +using Moq; +using Xunit; + +namespace Dapr.Jobs.Test; + +public sealed class DaprJobsGrpcClientTests +{ + + [Fact] + public void ScheduleJobAsync_RepeatsCannotBeLessThanZero() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync("MyJob", DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_JobNameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync(null, DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_JobNameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync(string.Empty, DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_ScheduleCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync("MyJob", new DaprJobSchedule(string.Empty), null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_TtlCannotBeEarlierThanStartingFrom() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + var date = DateTime.UtcNow.AddDays(10); + var earlierDate = date.AddDays(-2); + await client.ScheduleJobAsync("MyJob", DaprJobSchedule.Daily, null, date, null, earlierDate, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void GetJobAsync_NameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.GetJobAsync(null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void GetJobAsync_NameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.GetJobAsync(string.Empty, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void DeleteJobAsync_NameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.DeleteJobAsync(null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void DeleteJobAsync_NameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.DeleteJobAsync(string.Empty, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + private sealed record TestPayload(string Name, string Color); +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..34d900aeb --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Net.Http; +using Dapr.Jobs.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class DaprJobsServiceCollectionExtensionsTest +{ + [Fact] + public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() + { + var services = new ServiceCollection(); + + var clientBuilder = new Action(builder => + builder.UseDaprApiToken("abc")); + + services.AddDaprJobsClient(); //Sets a default API token value of an empty string + services.AddDaprJobsClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; + + Assert.Null(daprJobClient!.apiTokenHeader); + Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprJobsClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprJobsClient = serviceProvider.GetService(); + Assert.NotNull(daprJobsClient); + } + + [Fact] + public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprJobsClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var daprApiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(daprApiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; + + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); + Assert.NotNull(client.apiTokenHeader); + Assert.True(client.apiTokenHeader.HasValue); + Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); + Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + } + + private class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs new file mode 100644 index 000000000..fa4d094e1 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTests.cs @@ -0,0 +1,179 @@ +// ------------------------------------------------------------------------ +// 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.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class EndpointRouteBuilderExtensionsTest +{ + [Fact] + public async Task MapDaprScheduledJobHandler_ValidRequest_ExecutesAction() + { + var server = CreateTestServer(); + var client = server.CreateClient(); + + var serializedPayload = JsonSerializer.Serialize(new SamplePayload("Dapr", 789)); + var serializedPayloadBytes = Encoding.UTF8.GetBytes(serializedPayload); + var jobDetails = new DaprJobDetails(new DaprJobSchedule("0 0 * * *")) + { + RepeatCount = 5, + DueTime = DateTimeOffset.UtcNow, + Ttl = DateTimeOffset.UtcNow.AddHours(1), + Payload = serializedPayloadBytes + }; + var content = new StringContent(JsonSerializer.Serialize(jobDetails), Encoding.UTF8, "application/json"); + + const string jobName = "testJob"; + var response = await client.PostAsync($"/job/{jobName}", content); + + response.EnsureSuccessStatusCode(); + + //Validate the job name and payload + var validator = server.Services.GetRequiredService(); + Assert.Equal(jobName, validator.JobName); + Assert.Equal(serializedPayload, validator.SerializedPayload); + } + + [Fact] + public async Task MapDaprScheduleJobHandler_HandleMissingCancellationToken() + { + var server = CreateTestServer2(); + var client = server.CreateClient(); + + var serializedPayload = JsonSerializer.Serialize(new SamplePayload("Dapr", 789)); + var serializedPayloadBytes = Encoding.UTF8.GetBytes(serializedPayload); + var jobDetails = new DaprJobDetails(new DaprJobSchedule("0 0 * * *")) + { + RepeatCount = 5, + DueTime = DateTimeOffset.UtcNow, + Ttl = DateTimeOffset.UtcNow.AddHours(1), + Payload = serializedPayloadBytes + }; + var content = new StringContent(JsonSerializer.Serialize(jobDetails), Encoding.UTF8, "application/json"); + + const string jobName = "testJob"; + var response = await client.PostAsync($"/job/{jobName}", content); + + response.EnsureSuccessStatusCode(); + + //Validate the job name and payload + var validator = server.Services.GetRequiredService(); + Assert.Equal(jobName, validator.JobName); + Assert.Equal(serializedPayload, validator.SerializedPayload); + } + + + [Fact] + public async Task MapDaprScheduledJobHandler_InvalidPayload() + { + // Arrange + var server = CreateTestServer(); + var client = server.CreateClient(); + + var content = new StringContent("", Encoding.UTF8, "application/json"); + + // Act + const string jobName = "testJob"; + var response = await client.PostAsync($"/job/{jobName}", content); + + var validator = server.Services.GetRequiredService(); + Assert.Equal(jobName, validator.JobName); + Assert.Null(validator.SerializedPayload); + } + + private sealed record SamplePayload(string Name, int Count); + + public sealed class Validator + { + public string? JobName { get; set; } + + public string? SerializedPayload { get; set; } + } + + private static TestServer CreateTestServer() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddRouting(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapDaprScheduledJobHandler(async (string? jobName, DaprJobDetails? jobDetails, Validator validator, CancellationToken cancellationToken) => + { + if (jobName is not null) + validator.JobName = jobName; + if (jobDetails?.Payload is not null) + { + var payloadString = Encoding.UTF8.GetString(jobDetails.Payload); + validator.SerializedPayload = payloadString; + } + await Task.CompletedTask; + }); + }); + }); + + return new TestServer(builder); + } + + private static TestServer CreateTestServer2() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddSingleton(); + services.AddRouting(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapDaprScheduledJobHandler(async (string? jobName, Validator validator, DaprJobDetails? jobDetails) => + { + if (jobName is not null) + validator.JobName = jobName; + if (jobDetails?.Payload is not null) + { + var payloadString = Encoding.UTF8.GetString(jobDetails.Payload); + validator.SerializedPayload = payloadString; + } + await Task.CompletedTask; + }); + }); + }); + + return new TestServer(builder); + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000..8c25de115 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Dapr.Jobs.Extensions; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class StringExtensionsTests +{ + [Fact] + public void EndsWithAny_ContainsMatch() + { + const string testValue = "@weekly"; + var result = testValue.EndsWithAny(new List + { + "every", + "monthly", + "weekly", + "daily", + "midnight", + "hourly" + }, StringComparison.InvariantCulture); + Assert.True(result); + } + + [Fact] + public void EndsWithAny_DoesNotContainMatch() + { + const string testValue = "@weekly"; + var result = testValue.EndsWithAny(new List { "every", "monthly", "daily", "midnight", "hourly" }, StringComparison.InvariantCulture); + Assert.False(result); + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs new file mode 100644 index 000000000..1e888a841 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTests.cs @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------ +// 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.Jobs.Test.Extensions; + +public class TimeSpanExtensionsTest +{ + [Theory] + [InlineData("5h", true)] + [InlineData("5m", true)] + [InlineData("10s", true)] + [InlineData("30q", true)] + [InlineData("5h2m", true)] + [InlineData("2m44s", true)] + [InlineData("49s28q", true)] + [InlineData("21m2s9q", true)] + [InlineData("9h17m10s55q", true)] + [InlineData("12z", false)] + [InlineData("60ms", true)] + [InlineData("", false)] + public void IsDurationString_Validate(string original, bool expectedResult) + { + var actualResult = original.IsDurationString(); + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void ToDurationString_ValidateHours() + { + var fourHours = TimeSpan.FromHours(4); + var result = fourHours.ToDurationString(); + + Assert.Equal("4h", result); + } + + [Fact] + public void ToDurationString_ValidateMinutes() + { + var elevenMinutes = TimeSpan.FromMinutes(11); + var result = elevenMinutes.ToDurationString(); + + Assert.Equal("11m", result); + } + + [Fact] + public void ToDurationString_ValidateSeconds() + { + var fortySeconds = TimeSpan.FromSeconds(40); + var result = fortySeconds.ToDurationString(); + + Assert.Equal("40s", result); + } + + [Fact] + public void ToDurationString_ValidateMilliseconds() + { + var tenMilliseconds = TimeSpan.FromMilliseconds(10); + var result = tenMilliseconds.ToDurationString(); + + Assert.Equal("10ms", result); + } + + [Fact] + public void ToDurationString_HoursAndMinutes() + { + var ninetyMinutes = TimeSpan.FromMinutes(90); + var result = ninetyMinutes.ToDurationString(); + + Assert.Equal("1h30m", result); + } + + [Fact] + public void ToDurationString_Combined() + { + var time = TimeSpan.FromHours(2) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(24) + + TimeSpan.FromMilliseconds(28); + var result = time.ToDurationString(); + + Assert.Equal("2h4m24s28ms", result); + } + + [Fact] + public void FromDurationString_AllSegments() + { + const string interval = "13h57m4s10ms"; + var result = interval.FromDurationString(); + + Assert.Equal(13, result.Hours); + Assert.Equal(57, result.Minutes); + Assert.Equal(4, result.Seconds); + Assert.Equal(10, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments1() + { + const string interval = "5h12ms"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Hours); + Assert.Equal(12, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments2() + { + const string interval = "5m"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Minutes); + } + + [Fact] + public void FromDurationString_LimitedSegments3() + { + const string interval = "16s43ms"; + var result = interval.FromDurationString(); + + Assert.Equal(16, result.Seconds); + Assert.Equal(43, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments4() + { + const string interval = "4h32m16s"; + var result = interval.FromDurationString(); + + Assert.Equal(4, result.Hours); + Assert.Equal(32, result.Minutes); + Assert.Equal(16, result.Seconds); + } +} diff --git a/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs new file mode 100644 index 000000000..17eb48362 --- /dev/null +++ b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs @@ -0,0 +1,172 @@ +// ------------------------------------------------------------------------ +// 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 Dapr.Jobs.Models; +using Xunit; + +namespace Dapr.Jobs.Test.Models; + +public sealed class DaprJobScheduleTests +{ + [Fact] + public void FromDuration_Validate() + { + var schedule = DaprJobSchedule.FromDuration(new TimeSpan(12, 8, 16)); + Assert.Equal("12h8m16s", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_Cron() + { + const string cronExpression = "*/5 1-5 * * JAN,FEB WED-SAT"; + + var schedule = DaprJobSchedule.FromExpression(cronExpression); + Assert.Equal(cronExpression, schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Yearly() + { + var schedule = DaprJobSchedule.Yearly; + Assert.Equal("@yearly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Monthly() + { + var schedule = DaprJobSchedule.Monthly; + Assert.Equal("@monthly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Weekly() + { + var schedule = DaprJobSchedule.Weekly; + Assert.Equal("@weekly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Daily() + { + var schedule = DaprJobSchedule.Daily; + Assert.Equal("@daily", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Midnight() + { + var schedule = DaprJobSchedule.Midnight; + Assert.Equal("@midnight", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Hourly() + { + var schedule = DaprJobSchedule.Hourly; + Assert.Equal("@hourly", schedule.ExpressionValue); + } + + [Fact] + public void FromCronExpression() + { + var schedule = DaprJobSchedule.FromCronExpression(new CronExpressionBuilder() + .On(OnCronPeriod.Second, 15) + .Every(EveryCronPeriod.Minute, 2) + .Every(EveryCronPeriod.Hour, 4) + .Through(ThroughCronPeriod.DayOfMonth, 2, 13) + .Through(DayOfWeek.Monday, DayOfWeek.Saturday) + .On(MonthOfYear.June, MonthOfYear.August, MonthOfYear.January)); + + Assert.Equal("15 */2 */4 2-13 JAN,JUN,AUG MON-SAT", schedule.ExpressionValue); + } + + [Fact] + public void IsPointInTimeExpression() + { + var schedule = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddDays(2)); + Assert.True(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + } + + [Fact] + public void IsDurationExpression() + { + var schedule = DaprJobSchedule.FromDuration(TimeSpan.FromHours(2)); + Assert.True(schedule.IsDurationExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + } + + [Fact] + public void IsPrefixedPeriodExpression() + { + var schedule = DaprJobSchedule.Weekly; + Assert.True(schedule.IsPrefixedPeriodExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + } + + [Theory] + [InlineData("5h")] + [InlineData("5h5m")] + [InlineData("5h2m12s")] + [InlineData("5h9m22s27ms")] + [InlineData("42m12s28ms")] + [InlineData("19s2ms")] + [InlineData("292ms")] + [InlineData("5h23s")] + [InlineData("25m192ms")] + public void ValidateEveryExpression(string testValue) + { + var schedule = DaprJobSchedule.FromExpression($"@every {testValue}"); + Assert.True(schedule.IsPrefixedPeriodExpression); + } + + [Theory] + [InlineData("* * * * * *")] + [InlineData("5 12 16 7 * *")] + [InlineData("5 12 16 7 FEB *")] + [InlineData("5 12 16 7 * MON")] + [InlineData("5 12 16 7 JAN SAT")] + [InlineData("5 * * * FEB SUN")] + [InlineData("30 * * * * *")] + [InlineData("*/5 * * * * *")] + [InlineData("* */12 * * * *")] + [InlineData("* * */2 * * *")] + [InlineData("* * * */5 * *")] + [InlineData("30 15 6 */4 JAN,APR,AUG WED-FRI")] + [InlineData("*/10 */8 */2 */5 */3 */2")] + [InlineData("0 0 0 * * TUE,FRI")] + [InlineData("0 0 0 * * TUE-FRI")] + [InlineData("0 0 0 * OCT SAT")] + [InlineData("0 0 0 * OCT,DEC WED,SAT")] + [InlineData("0 0 0 * OCT-DEC WED-SAT")] + [InlineData("0-15 * * * * *")] + [InlineData("0-15 02-59 * * * *")] + [InlineData("1-14 2-59 20-23 * * *")] + [InlineData("0-59 0-59 0-23 1-31 1-12 0-6")] + [InlineData("*/1 2,4,5 * 2-9 JAN,FEB,DEC MON-WED")] + public void IsCronExpression(string testValue) + { + var schedule = DaprJobSchedule.FromExpression(testValue); + Assert.True(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + } +} diff --git a/test/Dapr.Jobs.Test/Responses/DaprJobDetailsTests.cs b/test/Dapr.Jobs.Test/Responses/DaprJobDetailsTests.cs new file mode 100644 index 000000000..0a416b00e --- /dev/null +++ b/test/Dapr.Jobs.Test/Responses/DaprJobDetailsTests.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Text.Json; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; + +namespace Dapr.Jobs.Test.Responses; + +public sealed class DaprJobDetailsTests +{ + [Fact] + public void ValidatePropertiesAreAsSet() + { + var payload = new TestPayload("Dapr", "Red"); + var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload); + + var dueTime = DateTimeOffset.UtcNow.AddDays(2); + var ttl = DateTimeOffset.UtcNow.AddMonths(3); + const int repeatCount = 15; + + var details = new DaprJobDetails(DaprJobSchedule.Midnight) + { + RepeatCount = repeatCount, + DueTime = dueTime, + Payload = payloadBytes, + Ttl = ttl + }; + + Assert.Equal(repeatCount, details.RepeatCount); + Assert.Equal(dueTime, details.DueTime); + Assert.Equal(ttl, details.Ttl); + Assert.Equal(payloadBytes, details.Payload); + } + + private sealed record TestPayload(string Name, string Color); +} From d1aa92b6e86ff03f95e2679900cfa8975ce33378 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 1 Nov 2024 12:23:17 -0500 Subject: [PATCH 29/69] Updated prereqs to specify .NET 6 and .NET 8 in v1.15 (#1398) Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- daprdocs/content/en/dotnet-sdk-docs/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 72e8b71d9..60a4a1a61 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 6+](https://dotnet.microsoft.com/download) installed +- [.NET 6](https://dotnet.microsoft.com/download) or [.NET 8+](https://dotnet.microsoft.com/download) installed ## Installation From a06605ce7b865f7f5fe92adc3a915869832ae245 Mon Sep 17 00:00:00 2001 From: Ruud van Falier <119449492+humandigital-ruud@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:00:26 +0100 Subject: [PATCH 30/69] Refactor DaprWorkflowClientBuilderFactory and WorkflowRuntimeOptions (#1244) This commit refactors the DaprWorkflowClientBuilderFactory and WorkflowRuntimeOptions classes. In DaprWorkflowClientBuilderFactory: - Added a new method, UseGrpcChannelOptions, to allow the use of custom GrpcChannelOptions for creating the GrpcChannel. - Updated the UseGrpc method to use the GrpcChannelOptions provided by the WorkflowRuntimeOptions. In WorkflowRuntimeOptions: - Added a new property, GrpcChannelOptions, to store the custom GrpcChannelOptions. - Added a new method, UseGrpcChannelOptions, to set the GrpcChannelOptions. These changes improve the flexibility and customization options for the Dapr workflow client. Signed-off-by: Michiel van Praat Co-authored-by: Michiel van Praat Signed-off-by: Siri Varma Vegiraju --- .../DaprWorkflowClientBuilderFactory.cs | 24 ++++++++++++++----- src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 16 +++++++++++++ test/Dapr.E2E.Test.App/Startup.cs | 20 ++++++++++++++++ test/Dapr.E2E.Test/DaprTestApp.cs | 1 + 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs index 7a854cf05..8e284baf3 100644 --- a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs +++ b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs @@ -41,7 +41,7 @@ public DaprWorkflowClientBuilderFactory(IConfiguration configuration, IHttpClien _httpClientFactory = httpClientFactory; _services = services; } - + /// /// Responsible for building the client itself. /// @@ -50,17 +50,25 @@ public void CreateClientBuilder(Action configure) { _services.AddDurableTaskClient(builder => { + WorkflowRuntimeOptions options = new(); + configure?.Invoke(options); + var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); - + var httpClient = _httpClientFactory.CreateClient(); if (!string.IsNullOrWhiteSpace(apiToken)) { - httpClient.DefaultRequestHeaders.Add( "Dapr-Api-Token", apiToken); + httpClient.DefaultRequestHeaders.Add("Dapr-Api-Token", apiToken); } - builder.UseGrpc(GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + var channelOptions = options.GrpcChannelOptions ?? new GrpcChannelOptions + { + HttpClient = httpClient + }; + + builder.UseGrpc(GrpcChannel.ForAddress(grpcEndpoint, channelOptions)); builder.RegisterDirectly(); }); @@ -81,8 +89,12 @@ public void CreateClientBuilder(Action configure) httpClient.DefaultRequestHeaders.Add("Dapr-Api-Token", apiToken); } - builder.UseGrpc( - GrpcChannel.ForAddress(grpcEndpoint, new GrpcChannelOptions { HttpClient = httpClient })); + var channelOptions = options.GrpcChannelOptions ?? new GrpcChannelOptions + { + HttpClient = httpClient + }; + + builder.UseGrpc(GrpcChannel.ForAddress(grpcEndpoint, channelOptions)); } else { diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index e2fbf913a..9afdfb5e7 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Grpc.Net.Client; + namespace Dapr.Workflow { using System; @@ -29,6 +31,11 @@ public sealed class WorkflowRuntimeOptions ///
readonly Dictionary> factories = new(); + /// + /// Override GrpcChannelOptions. + /// + internal GrpcChannelOptions? GrpcChannelOptions { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -117,6 +124,15 @@ public void RegisterActivity() where TActivity : class, IWorkflowActi WorkflowLoggingService.LogActivityName(name); }); } + + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + public void UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + } /// /// Method to add workflows and activities to the registry. diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 05c633000..19de79714 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -31,6 +31,7 @@ namespace Dapr.E2E.Test using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Serilog; + using Grpc.Net.Client; /// /// Startup class. @@ -96,6 +97,25 @@ public void ConfigureServices(IServiceCollection services) return Task.FromResult($"We are shipping {input} to the customer using our hoard of drones!"); }); }); + services.AddDaprWorkflow(options => + { + // Example of registering a "StartOrder" workflow function + options.RegisterWorkflow("StartLargeOrder", implementation: async (context, input) => + { + var itemToPurchase = input; + itemToPurchase = await context.WaitForExternalEventAsync("FinishLargeOrder"); + return itemToPurchase; + }); + options.RegisterActivity("FinishLargeOrder", implementation: (context, input) => + { + return Task.FromResult($"We are finishing, it's huge!"); + }); + options.UseGrpcChannelOptions(new GrpcChannelOptions + { + MaxReceiveMessageSize = 32 * 1024 * 1024, + MaxSendMessageSize = 32 * 1024 * 1024 + }); + }); services.AddActors(options => { options.UseJsonSerialization = JsonSerializationEnabled; diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 83f9948ac..152aeee98 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -58,6 +58,7 @@ public DaprTestApp(ITestOutputHelper output, string appId) "--components-path", componentsPath, "--config", configPath, "--log-level", "debug", + "--dapr-http-max-request-size", "32", }; From c464294de953081adda746256f17b54f3dff7bc2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 Nov 2024 01:28:02 -0600 Subject: [PATCH 31/69] Fix for DI registration not completing as expected (#1386) * Tentative fix for DI registration not completing as expected Signed-off-by: Whit Waldo * Making injected IConfiguration optional as it might not be populated if user isn't utilizing ASP.NET Core from caller Signed-off-by: Whit Waldo * Fixed DI injection issue Signed-off-by: Whit Waldo * Removed registration of DaprWorkflowClientBuilderFactory Signed-off-by: Whit Waldo * Updated field names for consistency Signed-off-by: Whit Waldo * Minor formatting changes Signed-off-by: Whit Waldo * Fixed build error caused by bad merge resolution Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../DaprWorkflowClientBuilderFactory.cs | 36 ++++----- .../WorkflowServiceCollectionExtensions.cs | 74 ++++++++++--------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs index 8e284baf3..760dadd8b 100644 --- a/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs +++ b/src/Dapr.Workflow/DaprWorkflowClientBuilderFactory.cs @@ -19,8 +19,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -#nullable enable - namespace Dapr.Workflow; /// @@ -28,35 +26,33 @@ namespace Dapr.Workflow; /// internal sealed class DaprWorkflowClientBuilderFactory { - private readonly IConfiguration _configuration; - private readonly IHttpClientFactory _httpClientFactory; - private readonly IServiceCollection _services; + private readonly IConfiguration? configuration; + private readonly IHttpClientFactory httpClientFactory; /// /// Constructor used to inject the required types into the factory. /// - public DaprWorkflowClientBuilderFactory(IConfiguration configuration, IHttpClientFactory httpClientFactory, IServiceCollection services) + public DaprWorkflowClientBuilderFactory(IConfiguration? configuration, IHttpClientFactory httpClientFactory) { - _configuration = configuration; - _httpClientFactory = httpClientFactory; - _services = services; + this.configuration = configuration; + this.httpClientFactory = httpClientFactory; } /// /// Responsible for building the client itself. /// /// - public void CreateClientBuilder(Action configure) + public void CreateClientBuilder(IServiceCollection services, Action configure) { - _services.AddDurableTaskClient(builder => + services.AddDurableTaskClient(builder => { WorkflowRuntimeOptions options = new(); - configure?.Invoke(options); + configure.Invoke(options); - var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); - var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + var apiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = httpClientFactory.CreateClient(); if (!string.IsNullOrWhiteSpace(apiToken)) { @@ -72,17 +68,17 @@ public void CreateClientBuilder(Action configure) builder.RegisterDirectly(); }); - _services.AddDurableTaskWorker(builder => + services.AddDurableTaskWorker(builder => { WorkflowRuntimeOptions options = new(); - configure?.Invoke(options); + configure.Invoke(options); - var apiToken = DaprDefaults.GetDefaultDaprApiToken(_configuration); - var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(_configuration); + var apiToken = DaprDefaults.GetDefaultDaprApiToken(configuration); + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(configuration); if (!string.IsNullOrEmpty(grpcEndpoint)) { - var httpClient = _httpClientFactory.CreateClient(); + var httpClient = httpClientFactory.CreateClient(); if (!string.IsNullOrWhiteSpace(apiToken)) { diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 3c19583aa..5c10a776e 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -11,51 +11,55 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow -{ - using System; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; +using System; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Workflow; +/// +/// Contains extension methods for using Dapr Workflow with dependency injection. +/// +public static class WorkflowServiceCollectionExtensions +{ /// - /// Contains extension methods for using Dapr Workflow with dependency injection. + /// Adds Dapr Workflow support to the service collection. /// - public static class WorkflowServiceCollectionExtensions + /// The . + /// A delegate used to configure actor options and register workflow functions. + public static IServiceCollection AddDaprWorkflow( + this IServiceCollection serviceCollection, + Action configure) { - /// - /// Adds Dapr Workflow support to the service collection. - /// - /// The . - /// A delegate used to configure actor options and register workflow functions. - public static IServiceCollection AddDaprWorkflow( - this IServiceCollection serviceCollection, - Action configure) + if (serviceCollection == null) { - if (serviceCollection == null) - { - throw new ArgumentNullException(nameof(serviceCollection)); - } + throw new ArgumentNullException(nameof(serviceCollection)); + } - serviceCollection.TryAddSingleton(); - serviceCollection.AddHttpClient(); + serviceCollection.TryAddSingleton(); + serviceCollection.AddHttpClient(); #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); #pragma warning restore CS0618 // Type or member is obsolete - serviceCollection.AddHostedService(); - serviceCollection.TryAddSingleton(); - serviceCollection.AddDaprClient(); + serviceCollection.AddHostedService(); + serviceCollection.TryAddSingleton(); + serviceCollection.AddDaprClient(); - serviceCollection.AddOptions().Configure(configure); - - serviceCollection.AddSingleton(c => - { - var factory = c.GetRequiredService(); - factory.CreateClientBuilder(configure); - return new object(); //Placeholder as actual registration is performed inside factory - }); - - return serviceCollection; + serviceCollection.AddOptions().Configure(configure); + + //Register the factory and force resolution so the Durable Task client and worker can be registered + using (var scope = serviceCollection.BuildServiceProvider().CreateScope()) + { + var httpClientFactory = scope.ServiceProvider.GetRequiredService(); + var configuration = scope.ServiceProvider.GetService(); + + var factory = new DaprWorkflowClientBuilderFactory(configuration, httpClientFactory); + factory.CreateClientBuilder(serviceCollection, configure); } + + return serviceCollection; } } From 372d428c47f77a43967b3f9ad8783982001d7478 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 Nov 2024 11:57:21 -0600 Subject: [PATCH 32/69] Add .NET client for pub/sub support - streaming subscriptions (#1381) * Building out Dapr.Messaging and test project for streaming pubsub subscriptions Signed-off-by: Whit Waldo * Added copyright notices Signed-off-by: Whit Waldo * Minor stylistic updates Signed-off-by: Whit Waldo * Added generic client builder to support publish/subscribe client builder Signed-off-by: Whit Waldo * Tweaked XML comment Signed-off-by: Whit Waldo * Added several unit tests for the generic client builder Signed-off-by: Whit Waldo * Updated to include latest review changes: - Added lock so that while we guarantee the method is called only once, it should be thread-safe now - Marked PublishSubscribeReceiver as internal so its members aren't part of the public API - Updated TopicMessage to use IReadOnlyDictionary Signed-off-by: Whit Waldo * Switched to interlock exchange instead of lock to slightly simplify code Signed-off-by: Whit Waldo * Added sample project Signed-off-by: Whit Waldo * Minor changes to unit test Signed-off-by: Whit Waldo * Deleted protos folder Signed-off-by: Whit Waldo * Using lowercase protos dir name Signed-off-by: Whit Waldo * Added registration extension methods Signed-off-by: Whit Waldo * Updated example to use DI registration Signed-off-by: Whit Waldo * Added default cancellation token Signed-off-by: Whit Waldo * Passing stream into method instead of creating it twice Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 98 +++--- all.sln | 20 ++ .../StreamingSubscriptionExample/Program.cs | 35 ++ .../StreamingSubscriptionExample.csproj | 14 + src/Dapr.Messaging/Dapr.Messaging.csproj | 22 ++ .../DaprPublishSubscribeClient.cs | 31 ++ .../DaprPublishSubscribeClientBuilder.cs | 47 +++ .../DaprPublishSubscribeGrpcClient.cs | 49 +++ .../DaprSubscriptionOptions.cs | 44 +++ ...ishSubscribeServiceCollectionExtensions.cs | 38 +++ .../PublishSubscribe/MessageHandlingPolicy.cs | 23 ++ .../PublishSubscribeReceiver.cs | 313 ++++++++++++++++++ .../PublishSubscribe/TopicMessage.cs | 46 +++ .../PublishSubscribe/TopicMessageHandler.cs | 23 ++ .../PublishSubscribe/TopicResponseAction.cs | 34 ++ .../DaprGenericClientBuilderTest.cs | 94 ++++++ .../Dapr.Messaging.Test.csproj | 42 +++ .../MessageHandlingPolicyTest.cs | 55 +++ test/Dapr.Messaging.Test/protos/test.proto | 32 ++ 19 files changed, 1011 insertions(+), 49 deletions(-) create mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs create mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj create mode 100644 src/Dapr.Messaging/Dapr.Messaging.csproj create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs create mode 100644 src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs create mode 100644 test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs create mode 100644 test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj create mode 100644 test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs create mode 100644 test/Dapr.Messaging.Test/protos/test.proto diff --git a/Directory.Packages.props b/Directory.Packages.props index 4a9c47ad4..e5e60cd7b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,51 +1,51 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all.sln b/all.sln index 6e55f247b..3b9959902 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{0EAE36A1-B578-4F13-A113-7A477ECA1BDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamingSubscriptionExample", "examples\Client\PublishSubscribe\StreamingSubscriptionExample\StreamingSubscriptionExample.csproj", "{290D1278-F613-4DF3-9DF5-F37E38CDC363}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{C8BB6A85-A7EA-40C0-893D-F36F317829B3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{BF9828E9-5597-4D42-AA6E-6E6C12214204}" @@ -311,6 +316,18 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Release|Any CPU.Build.0 = Release|Any CPU + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA}.Release|Any CPU.Build.0 = Release|Any CPU + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.Build.0 = Debug|Any CPU + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.ActiveCfg = Release|Any CPU + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -379,6 +396,9 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs new file mode 100644 index 000000000..ac00d8798 --- /dev/null +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs @@ -0,0 +1,35 @@ +using System.Text; +using Dapr.Messaging.PublishSubscribe; +using Dapr.Messaging.PublishSubscribe.Extensions; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDaprPubSubClient(); +var app = builder.Build(); + +//Process each message returned from the subscription +Task HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default) +{ + try + { + //Do something with the message + Console.WriteLine(Encoding.UTF8.GetString(message.Data.Span)); + return Task.FromResult(TopicResponseAction.Success); + } + catch + { + return Task.FromResult(TopicResponseAction.Retry); + } +} + +var messagingClient = app.Services.GetRequiredService(); + +//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic", + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)), + HandleMessageAsync, cancellationTokenSource.Token); + +await Task.Delay(TimeSpan.FromMinutes(1)); + +//When you're done with the subscription, simply dispose of it +await subscription.DisposeAsync(); diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj new file mode 100644 index 000000000..4ad620d00 --- /dev/null +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/StreamingSubscriptionExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/src/Dapr.Messaging/Dapr.Messaging.csproj b/src/Dapr.Messaging/Dapr.Messaging.csproj new file mode 100644 index 000000000..0c7f159a4 --- /dev/null +++ b/src/Dapr.Messaging/Dapr.Messaging.csproj @@ -0,0 +1,22 @@ + + + + This package contains the reference assemblies for developing messaging services using Dapr. + enable + enable + Dapr.Messaging + Dapr Messaging SDK + Dapr Messaging SDK for building applications that utilize messaging components. + alpha + + + + + + + + + + + + diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs new file mode 100644 index 000000000..8fbec2dfe --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClient.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// The base implementation of a Dapr pub/sub client. +/// +public abstract class DaprPublishSubscribeClient +{ + /// + /// Dynamically subscribes to a Publish/Subscribe component and topic. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Configuration options. + /// The delegate reflecting the action to take upon messages received by the subscription. + /// Cancellation token. + /// + public abstract Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs new file mode 100644 index 000000000..b94bc5cdf --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Microsoft.Extensions.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Builds a . +/// +public sealed class DaprPublishSubscribeClientBuilder : DaprGenericClientBuilder +{ + /// + /// Used to initialize a new instance of the . + /// + /// An optional instance of . + public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : base(configuration) + { + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public override DaprPublishSubscribeClient Build() + { + var daprClientDependencies = BuildDaprClientDependencies(); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + + return new DaprPublishSubscribeGrpcClient(client); + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs new file mode 100644 index 000000000..df6ccdcfe --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------ +// 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 P = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient +{ + private readonly P.DaprClient daprClient; + + /// + /// Creates a new instance of a + /// + public DaprPublishSubscribeGrpcClient(P.DaprClient client) + { + daprClient = client; + } + + /// + /// Dynamically subscribes to a Publish/Subscribe component and topic. + /// + /// The name of the Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Configuration options. + /// The delegate reflecting the action to take upon messages received by the subscription. + /// Cancellation token. + /// + public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) + { + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient); + await receiver.SubscribeAsync(cancellationToken); + return receiver; + } +} + diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs new file mode 100644 index 000000000..73838b605 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/DaprSubscriptionOptions.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Options used to configure the dynamic Dapr subscription. +/// +/// Describes the policy to take on messages that have not been acknowledged within the timeout period. +public sealed record DaprSubscriptionOptions(MessageHandlingPolicy MessageHandlingPolicy) +{ + /// + /// Subscription metadata. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + + /// + /// The optional name of the dead-letter topic to send unprocessed messages to. + /// + public string? DeadLetterTopic { get; init; } + + /// + /// If populated, this reflects the maximum number of messages that can be queued for processing on the replica. By default, + /// no maximum boundary is enforced. + /// + public int? MaximumQueuedMessages { get; init; } + + /// + /// The maximum amount of time to take to dispose of acknowledgement messages after the cancellation token has + /// been signaled. + /// + public TimeSpan MaximumCleanupTimeout { get; init; } = TimeSpan.FromSeconds(30); +} + diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs new file mode 100644 index 000000000..bc60c5880 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Messaging.PublishSubscribe.Extensions; + +/// +/// Contains extension methods for using Dapr Publish/Subscribe with dependency injection. +/// +public static class PublishSubscribeServiceCollectionExtensions +{ + /// + /// Adds Dapr Publish/Subscribe support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Register the IHttpClientFactory implementation + services.AddHttpClient(); + + services.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprPublishSubscribeClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + + return services; + } +} diff --git a/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs new file mode 100644 index 000000000..de6882095 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/MessageHandlingPolicy.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Defines the policy for handling streaming message subscriptions, including retry logic and timeout settings. +/// +/// The duration to wait before timing out a message handling operation. +/// The default action to take when a message handling operation times out. +public sealed record MessageHandlingPolicy(TimeSpan TimeoutDuration, TopicResponseAction DefaultResponseAction); + diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs new file mode 100644 index 000000000..886d57006 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -0,0 +1,313 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Threading.Channels; +using Dapr.AppCallback.Autogen.Grpc.v1; +using Grpc.Core; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// A thread-safe implementation of a receiver for messages from a specified Dapr publish/subscribe component and +/// topic. +/// +internal sealed class PublishSubscribeReceiver : IAsyncDisposable +{ + /// + /// Provides options for the unbounded channel. + /// + private readonly static UnboundedChannelOptions UnboundedChannelOptions = new() + { + SingleWriter = true, SingleReader = true + }; + + /// + /// The name of the Dapr publish/subscribe component. + /// + private readonly string pubSubName; + /// + /// The name of the topic to subscribe to. + /// + private readonly string topicName; + /// + /// Options allowing the behavior of the receiver to be configured. + /// + private readonly DaprSubscriptionOptions options; + /// + /// A channel used to decouple the messages received from the sidecar to their consumption. + /// + private readonly Channel topicMessagesChannel; + /// + /// Maintains the various acknowledgements for each message. + /// + private readonly Channel acknowledgementsChannel = Channel.CreateUnbounded(UnboundedChannelOptions); + /// + /// The stream connection between this instance and the Dapr sidecar. + /// + private AsyncDuplexStreamingCall? clientStream; + /// + /// Used to ensure thread-safe operations against the stream. + /// + private readonly SemaphoreSlim semaphore = new(1, 1); + /// + /// The handler delegate responsible for processing the topic messages. + /// + private readonly TopicMessageHandler messageHandler; + /// + /// A reference to the DaprClient instance. + /// + private readonly P.Dapr.DaprClient client; + /// + /// Flag that prevents the developer from accidentally initializing the subscription more than once from the same receiver. + /// + private int hasInitialized; + /// + /// Flag that ensures the instance is only disposed a single time. + /// + private bool isDisposed; + + /// + /// Constructs a new instance of a instance. + /// + /// The name of the Dapr Publish/Subscribe component. + /// The name of the topic to subscribe to. + /// Options allowing the behavior of the receiver to be configured. + /// The delegate reflecting the action to take upon messages received by the subscription. + /// A reference to the DaprClient instance. + internal PublishSubscribeReceiver(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler handler, P.Dapr.DaprClient client) + { + this.client = client; + this.pubSubName = pubSubName; + this.topicName = topicName; + this.options = options; + this.messageHandler = handler; + topicMessagesChannel = options.MaximumQueuedMessages is > 0 + ? Channel.CreateBounded(new BoundedChannelOptions((int)options.MaximumQueuedMessages) + { + SingleReader = true, SingleWriter = true, FullMode = BoundedChannelFullMode.Wait + }) + : Channel.CreateUnbounded(UnboundedChannelOptions); + } + + /// + /// Dynamically subscribes to messages on a PubSub topic provided by the Dapr sidecar. + /// + /// Cancellation token. + /// An containing messages provided by the sidecar. + internal async Task SubscribeAsync(CancellationToken cancellationToken = default) + { + //Prevents the receiver from performing the subscribe operation more than once (as the multiple initialization messages would cancel the stream). + if (Interlocked.Exchange(ref hasInitialized, 1) == 1) + { + return; + } + + var stream = await GetStreamAsync(cancellationToken); + + //Retrieve the messages from the sidecar and write to the messages channel + var fetchMessagesTask = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken); + + //Process the messages as they're written to either channel + var acknowledgementProcessorTask = ProcessAcknowledgementChannelMessagesAsync(stream, cancellationToken); + var topicMessageProcessorTask = ProcessTopicChannelMessagesAsync(cancellationToken); + + try + { + await Task.WhenAll(fetchMessagesTask, acknowledgementProcessorTask, topicMessageProcessorTask); + } + catch (OperationCanceledException) + { + // Will be cleaned up during DisposeAsync + } + } + + /// + /// Retrieves or creates the bidirectional stream to the DaprClient for transacting pub/sub subscriptions. + /// + /// Cancellation token. + /// The stream connection. + private async Task> GetStreamAsync(CancellationToken cancellationToken) + { + await semaphore.WaitAsync(cancellationToken); + + try + { + return clientStream ??= client.SubscribeTopicEventsAlpha1(cancellationToken: cancellationToken); + } + finally + { + semaphore.Release(); + } + } + + /// + /// Acknowledges the indicated message back to the Dapr sidecar with an indicated behavior to take on the message. + /// + /// The identifier of the message the behavior is in reference to. + /// The behavior to take on the message as indicated by either the message handler or timeout message handling configuration. + /// Cancellation token. + /// + private async Task AcknowledgeMessageAsync(string messageId, TopicResponseAction behavior, CancellationToken cancellationToken) + { + var action = behavior switch + { + TopicResponseAction.Success => TopicEventResponse.Types.TopicEventResponseStatus.Success, + TopicResponseAction.Retry => TopicEventResponse.Types.TopicEventResponseStatus.Retry, + TopicResponseAction.Drop => TopicEventResponse.Types.TopicEventResponseStatus.Drop, + _ => throw new InvalidOperationException( + $"Unrecognized topic acknowledgement action: {behavior}") + }; + + var acknowledgement = new TopicAcknowledgement(messageId, action); + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement, cancellationToken); + } + + /// + /// Processes each acknowledgement from the acknowledgement channel reader as it's populated. + /// + /// The stream used to interact with the Dapr sidecar. + /// Cancellation token. + private async Task ProcessAcknowledgementChannelMessagesAsync(AsyncDuplexStreamingCall messageStream, CancellationToken cancellationToken) + { + await foreach (var acknowledgement in acknowledgementsChannel.Reader.ReadAllAsync(cancellationToken)) + { + await messageStream.RequestStream.WriteAsync(new P.SubscribeTopicEventsRequestAlpha1 + { + EventProcessed = new P.SubscribeTopicEventsRequestProcessedAlpha1 + { + Id = acknowledgement.MessageId, + Status = new TopicEventResponse { Status = acknowledgement.Action } + } + }, cancellationToken); + } + } + + /// + /// Processes each topic messages from the channel as it's populated. + /// + /// Cancellation token. + private async Task ProcessTopicChannelMessagesAsync(CancellationToken cancellationToken) + { + await foreach (var message in topicMessagesChannel.Reader.ReadAllAsync(cancellationToken)) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(options.MessageHandlingPolicy.TimeoutDuration); + + //Evaluate the message and return an acknowledgement result + var messageAction = await messageHandler(message, cts.Token); + + try + { + //Share the result with the sidecar + await AcknowledgeMessageAsync(message.Id, messageAction, cancellationToken); + } + catch (OperationCanceledException) + { + //Acknowledge the message using the configured default response action + await AcknowledgeMessageAsync(message.Id, options.MessageHandlingPolicy.DefaultResponseAction, cancellationToken); + } + } + } + + /// + /// Retrieves the subscription stream data from the Dapr sidecar. + /// + /// The stream connection to and from the Dapr sidecar instance. + /// The channel writer instance. + /// Cancellation token. + private async Task FetchDataFromSidecarAsync( + AsyncDuplexStreamingCall stream, + ChannelWriter channelWriter, CancellationToken cancellationToken) + { + //Build out the initial topic events request + var initialRequest = new P.SubscribeTopicEventsRequestInitialAlpha1() + { + PubsubName = pubSubName, DeadLetterTopic = options.DeadLetterTopic ?? string.Empty, Topic = topicName + }; + + if (options.Metadata.Count > 0) + { + foreach (var (key, value) in options.Metadata) + { + initialRequest.Metadata.Add(key, value); + } + } + + //Send this request to the Dapr sidecar + await stream.RequestStream.WriteAsync( + new P.SubscribeTopicEventsRequestAlpha1 { InitialRequest = initialRequest }, cancellationToken); + + //Each time a message is received from the stream, push it into the topic messages channel + await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) + { + var message = + new TopicMessage(response.EventMessage.Id, response.EventMessage.Source, response.EventMessage.Type, + response.EventMessage.SpecVersion, response.EventMessage.DataContentType, + response.EventMessage.Topic, response.EventMessage.PubsubName) + { + Path = response.EventMessage.Path, + Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) + }; + + try + { + await channelWriter.WaitToWriteAsync(cancellationToken); + await channelWriter.WriteAsync(message, cancellationToken); + } + catch (Exception) + { + // Handle being unable to write because the writer is completed due to an active DisposeAsync operation + } + } + } + + /// + /// Disposes the various resources associated with the instance. + /// + /// + public async ValueTask DisposeAsync() + { + if (isDisposed) + { + return; + } + + isDisposed = true; + + //Stop processing new events - we'll leave any messages yet unseen as unprocessed and + //Dapr will handle as necessary when they're not acknowledged + topicMessagesChannel.Writer.Complete(); + + //Flush the remaining acknowledgements, but start by marking the writer as complete so it doesn't receive any other messages either + acknowledgementsChannel.Writer.Complete(); + + try + { + //Process any remaining acknowledgements on the channel without exceeding the configured maximum clean up time + await acknowledgementsChannel.Reader.Completion.WaitAsync(options.MaximumCleanupTimeout); + } + catch (OperationCanceledException) + { + //Handled + } + } + + /// + /// Reflects the action to take on a given message identifier. + /// + /// The identifier of the message. + /// The action to take on the message in the acknowledgement request. + private sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); +} + diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs new file mode 100644 index 000000000..402a89e9f --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessage.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Google.Protobuf.WellKnownTypes; + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// A message retrieved from a Dapr publish/subscribe topic. +/// +/// The unique identifier of the topic message. +/// Identifies the context in which an event happened, such as the organization publishing the +/// event or the process that produced the event. The exact syntax and semantics behind the data +/// encoded in the URI is defined by the event producer. +/// The type of event related to the originating occurrence. +/// The spec version of the CloudEvents specification. +/// The content type of the data. +/// The name of the topic. +/// The name of the Dapr publish/subscribe component. +public sealed record TopicMessage(string Id, string Source, string Type, string SpecVersion, string DataContentType, string Topic, string PubSubName) +{ + /// + /// The content of the event. + /// + public ReadOnlyMemory Data { get; init; } + + /// + /// The matching path from the topic subscription/routes (if specified) for this event. + /// + public string? Path { get; init; } + + /// + /// A map of additional custom properties sent to the app. These are considered to be CloudEvent extensions. + /// + public IReadOnlyDictionary Extensions { get; init; } = new Dictionary(); +} diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs b/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs new file mode 100644 index 000000000..65b7abf01 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicMessageHandler.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// The handler delegate responsible for processing the topic message. +/// +/// The message request to process. +/// Cancellation token. +/// The acknowledgement behavior to report back to the pub/sub endpoint about the message. +public delegate Task TopicMessageHandler(TopicMessage request, + CancellationToken cancellationToken = default); diff --git a/src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs b/src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs new file mode 100644 index 000000000..5a34f4cc2 --- /dev/null +++ b/src/Dapr.Messaging/PublishSubscribe/TopicResponseAction.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Messaging.PublishSubscribe; + +/// +/// Describes the various actions that can be taken on a topic message. +/// +public enum TopicResponseAction +{ + /// + /// Indicates the message was processed successfully and should be deleted from the pub/sub topic. + /// + Success, + /// + /// Indicates a failure while processing the message and that the message should be resent for a retry. + /// + Retry, + /// + /// Indicates a failure while processing the message and that the message should be dropped or sent to the + /// dead-letter topic if specified. + /// + Drop +} diff --git a/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs new file mode 100644 index 000000000..d28b40058 --- /dev/null +++ b/test/Dapr.Common.Test/DaprGenericClientBuilderTest.cs @@ -0,0 +1,94 @@ +using System; +using System.Text.Json; +using Xunit; + +namespace Dapr.Common.Test; + +public class DaprGenericClientBuilderTest +{ + [Fact] + public void DaprGenericClientBuilder_ShouldUpdateHttpEndpoint_WhenHttpEndpointIsProvided() + { + // Arrange + var builder = new SampleDaprGenericClientBuilder(); + const string endpointValue = "http://sample-endpoint"; + + // Act + builder.UseHttpEndpoint(endpointValue); + + // Assert + Assert.Equal(endpointValue, builder.HttpEndpoint); + } + + [Fact] + public void DaprGenericClientBuilder_ShouldUpdateHttpEndpoint_WhenGrpcEndpointIsProvided() + { + // Arrange + var builder = new SampleDaprGenericClientBuilder(); + const string endpointValue = "http://sample-endpoint"; + + // Act + builder.UseGrpcEndpoint(endpointValue); + + // Assert + Assert.Equal(endpointValue, builder.GrpcEndpoint); + } + + [Fact] + public void DaprGenericClientBuilder_ShouldUpdateJsonSerializerOptions() + { + // Arrange + const int maxDepth = 8; + const bool writeIndented = true; + var builder = new SampleDaprGenericClientBuilder(); + var options = new JsonSerializerOptions + { + WriteIndented = writeIndented, + MaxDepth = maxDepth + }; + + // Act + builder.UseJsonSerializationOptions(options); + + // Assert + Assert.Equal(writeIndented, builder.JsonSerializerOptions.WriteIndented); + Assert.Equal(maxDepth, builder.JsonSerializerOptions.MaxDepth); + } + + [Fact] + public void DaprGenericClientBuilder_ShouldUpdateDaprApiToken() + { + // Arrange + const string apiToken = "abc123"; + var builder = new SampleDaprGenericClientBuilder(); + + // Act + builder.UseDaprApiToken(apiToken); + + // Assert + Assert.Equal(apiToken, builder.DaprApiToken); + } + + [Fact] + public void DaprGenericClientBuilder_ShouldUpdateTimeout() + { + // Arrange + var timeout = new TimeSpan(4, 2, 1, 2); + var builder = new SampleDaprGenericClientBuilder(); + + // Act + builder.UseTimeout(timeout); + + // Assert + Assert.Equal(timeout, builder.Timeout); + } + + private class SampleDaprGenericClientBuilder : DaprGenericClientBuilder + { + public override SampleDaprGenericClientBuilder Build() + { + // Implementation + throw new NotImplementedException(); + } + } +} diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj new file mode 100644 index 000000000..8f39e1713 --- /dev/null +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -0,0 +1,42 @@ + + + + enable + enable + false + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs new file mode 100644 index 000000000..0efb5e879 --- /dev/null +++ b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs @@ -0,0 +1,55 @@ +using Dapr.Messaging.PublishSubscribe; + +namespace Dapr.Messaging.Test.PublishSubscribe +{ + public class MessageHandlingPolicyTest + { + [Fact] + public void Test_MessageHandlingPolicy_Constructor() + { + var timeoutDuration = TimeSpan.FromMilliseconds(2000); + const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; + + var policy = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); + + Assert.Equal(timeoutDuration, policy.TimeoutDuration); + Assert.Equal(defaultResponseAction, policy.DefaultResponseAction); + } + + [Fact] + public void Test_MessageHandlingPolicy_Equality() + { + var timeSpan1 = TimeSpan.FromMilliseconds(1000); + var timeSpan2 = TimeSpan.FromMilliseconds(2000); + + var policy1 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); + var policy2 = new MessageHandlingPolicy(timeSpan1, TopicResponseAction.Success); + var policy3 = new MessageHandlingPolicy(timeSpan2, TopicResponseAction.Retry); + + Assert.Equal(policy1, policy2); // Value Equality + Assert.NotEqual(policy1, policy3); // Different values + } + + [Fact] + public void Test_MessageHandlingPolicy_Immutability() + { + var timeoutDuration = TimeSpan.FromMilliseconds(2000); + const TopicResponseAction defaultResponseAction = TopicResponseAction.Drop; + + var policy1 = new MessageHandlingPolicy(timeoutDuration, defaultResponseAction); + + var newTimeoutDuration = TimeSpan.FromMilliseconds(3000); + const TopicResponseAction newDefaultResponseAction = TopicResponseAction.Retry; + + // Creating a new policy with different values. + var policy2 = policy1 with + { + TimeoutDuration = newTimeoutDuration, DefaultResponseAction = newDefaultResponseAction + }; + + // Asserting that original policy is unaffected by changes made to new policy. + Assert.Equal(timeoutDuration, policy1.TimeoutDuration); + Assert.Equal(defaultResponseAction, policy1.DefaultResponseAction); + } + } +} diff --git a/test/Dapr.Messaging.Test/protos/test.proto b/test/Dapr.Messaging.Test/protos/test.proto new file mode 100644 index 000000000..b1c1ad8a9 --- /dev/null +++ b/test/Dapr.Messaging.Test/protos/test.proto @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +syntax = "proto3"; + +option csharp_namespace = "Dapr.Client.Autogen.Test.Grpc.v1"; + +message TestRun { + repeated TestCase tests = 1; +} + +message TestCase { + string name = 1; +} + +message Request { + string RequestParameter = 1; +} + +message Response { + string Name = 1; +} \ No newline at end of file From a3da0aac80fd154f47b118ef63054c2090065622 Mon Sep 17 00:00:00 2001 From: Mike Nguyen Date: Tue, 12 Nov 2024 17:03:12 +0000 Subject: [PATCH 33/69] ci: set fail-fast to false (#1405) Signed-off-by: Mike Nguyen Signed-off-by: Siri Varma Vegiraju --- .github/workflows/itests.yml | 1 + .github/workflows/sdk_build.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 36741ce7c..d06c12cd5 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -18,6 +18,7 @@ jobs: name: run integration tests runs-on: ubuntu-latest strategy: + fail-fast: false matrix: dotnet-version: ['6.0', '7.0', '8.0'] include: diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index fe935bfb8..5e6fd3532 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -41,6 +41,7 @@ jobs: name: Test .NET ${{ matrix.dotnet-version }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: dotnet-version: ['6.0', '7.0', '8.0'] include: From 3cbc50ff242033688632aba2af2de4be61e52f51 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 12 Nov 2024 14:08:21 -0700 Subject: [PATCH 34/69] Added async operations workflow sample (#1394) Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 5 +- all.sln | 10 +++- .../Activities/NotifyWarehouseActivity.cs | 33 ++++++++++++ .../Activities/ProcessPaymentActivity.cs | 33 ++++++++++++ .../Models/Transaction.cs | 19 +++++++ .../WorkflowAsyncOperations/Program.cs | 48 +++++++++++++++++ .../WorkflowAsyncOperations.csproj | 18 +++++++ .../Workflows/DemoWorkflow.cs | 52 +++++++++++++++++++ 8 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 examples/Workflow/WorkflowAsyncOperations/Activities/NotifyWarehouseActivity.cs create mode 100644 examples/Workflow/WorkflowAsyncOperations/Activities/ProcessPaymentActivity.cs create mode 100644 examples/Workflow/WorkflowAsyncOperations/Models/Transaction.cs create mode 100644 examples/Workflow/WorkflowAsyncOperations/Program.cs create mode 100644 examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj create mode 100644 examples/Workflow/WorkflowAsyncOperations/Workflows/DemoWorkflow.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index e5e60cd7b..772dd7c6e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,10 +29,11 @@ - + + + - diff --git a/all.sln b/all.sln index 3b9959902..34f0f6fdb 100644 --- a/all.sln +++ b/all.sln @@ -119,11 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowAsyncOperations", "examples\Workflow\WorkflowAsyncOperations\WorkflowAsyncOperations.csproj", "{00359961-0C50-4BB1-A794-8B06DE991639}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging", "src\Dapr.Messaging\Dapr.Messaging.csproj", "{0EAE36A1-B578-4F13-A113-7A477ECA1BDA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamingSubscriptionExample", "examples\Client\PublishSubscribe\StreamingSubscriptionExample\StreamingSubscriptionExample.csproj", "{290D1278-F613-4DF3-9DF5-F37E38CDC363}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{C8BB6A85-A7EA-40C0-893D-F36F317829B3}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{BF9828E9-5597-4D42-AA6E-6E6C12214204}" @@ -316,6 +319,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {00359961-0C50-4BB1-A794-8B06DE991639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00359961-0C50-4BB1-A794-8B06DE991639}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00359961-0C50-4BB1-A794-8B06DE991639}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00359961-0C50-4BB1-A794-8B06DE991639}.Release|Any CPU.Build.0 = Release|Any CPU {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -327,7 +334,7 @@ Global {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.Build.0 = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.ActiveCfg = Release|Any CPU - {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CPU + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CP {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -396,6 +403,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} diff --git a/examples/Workflow/WorkflowAsyncOperations/Activities/NotifyWarehouseActivity.cs b/examples/Workflow/WorkflowAsyncOperations/Activities/NotifyWarehouseActivity.cs new file mode 100644 index 000000000..2088ff810 --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/Activities/NotifyWarehouseActivity.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using WorkflowAsyncOperations.Models; + +namespace WorkflowAsyncOperations.Activities; + +internal sealed class NotifyWarehouseActivity : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override async Task RunAsync(WorkflowActivityContext context, Transaction input) + { + //Contact the warehouse to ship the product + await Task.Delay(TimeSpan.FromSeconds(8)); + return null; + } +} diff --git a/examples/Workflow/WorkflowAsyncOperations/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowAsyncOperations/Activities/ProcessPaymentActivity.cs new file mode 100644 index 000000000..1cd658899 --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/Activities/ProcessPaymentActivity.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using WorkflowAsyncOperations.Models; + +namespace WorkflowAsyncOperations.Activities; + +internal sealed class ProcessPaymentActivity : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override async Task RunAsync(WorkflowActivityContext context, Transaction input) + { + //Confirm payment with processor + await Task.Delay(TimeSpan.FromSeconds(10)); + return null; + } +} diff --git a/examples/Workflow/WorkflowAsyncOperations/Models/Transaction.cs b/examples/Workflow/WorkflowAsyncOperations/Models/Transaction.cs new file mode 100644 index 000000000..082c9564e --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/Models/Transaction.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace WorkflowAsyncOperations.Models; + +internal sealed record Transaction(decimal Value) +{ + public Guid CustomerId { get; init; } = Guid.NewGuid(); +} diff --git a/examples/Workflow/WorkflowAsyncOperations/Program.cs b/examples/Workflow/WorkflowAsyncOperations/Program.cs new file mode 100644 index 000000000..df46d1f9b --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/Program.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowAsyncOperations.Activities; +using WorkflowAsyncOperations.Models; +using WorkflowAsyncOperations.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + options.RegisterActivity(); + }); +}); + +var host = await builder.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +var instanceId = $"demo-workflow-{Guid.NewGuid().ToString()[..8]}"; +var transaction = new Transaction(16.58m); +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, transaction); + +//Poll for status updates every second +var status = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); +do +{ + Console.WriteLine($"Current status: {status.RuntimeStatus}, step: {status.ReadCustomStatusAs()}"); + status = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); +} while (!status.IsWorkflowCompleted); + +Console.WriteLine($"Workflow completed - {status.ReadCustomStatusAs()}"); diff --git a/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj b/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj new file mode 100644 index 000000000..a1350fa79 --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/WorkflowAsyncOperations.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowAsyncOperations/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowAsyncOperations/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..7d70ca632 --- /dev/null +++ b/examples/Workflow/WorkflowAsyncOperations/Workflows/DemoWorkflow.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 Dapr.Workflow; +using WorkflowAsyncOperations.Activities; +using WorkflowAsyncOperations.Models; + +namespace WorkflowAsyncOperations.Workflows; + +internal sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, Transaction input) + { + try + { + //Submit the transaction to the payment processor + context.SetCustomStatus("Processing payment..."); + await context.CallActivityAsync(nameof(ProcessPaymentActivity), input); + + + //Send the transaction details to the warehouse + context.SetCustomStatus("Contacting warehouse..."); + await context.CallActivityAsync(nameof(NotifyWarehouseActivity), input); + + context.SetCustomStatus("Success!"); + return true; + } + catch + { + //If anything goes wrong, return false + context.SetCustomStatus("Something went wrong"); + return false; + } + } +} + From c0a5a35d7dc3c3786a5e287722fbeabecf68296b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 13 Nov 2024 12:34:02 -0700 Subject: [PATCH 35/69] Added workflow example: Fan out/fan in (#1396) * Added workflow fan out/fan in example Signed-off-by: Whit Waldo * Added copyright headers Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ++++ .../Activities/NotifyActivity.cs | 31 ++++++++++++++ .../Workflow/WorkflowFanOutFanIn/Program.cs | 40 +++++++++++++++++++ .../WorkflowFanOutFanIn.csproj | 18 +++++++++ .../Workflows/DemoWorkflow.cs | 40 +++++++++++++++++++ 5 files changed, 136 insertions(+) create mode 100644 examples/Workflow/WorkflowFanOutFanIn/Activities/NotifyActivity.cs create mode 100644 examples/Workflow/WorkflowFanOutFanIn/Program.cs create mode 100644 examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj create mode 100644 examples/Workflow/WorkflowFanOutFanIn/Workflows/DemoWorkflow.cs diff --git a/all.sln b/all.sln index 34f0f6fdb..81f41d4d6 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowFanOutFanIn", "examples\Workflow\WorkflowFanOutFanIn\WorkflowFanOutFanIn.csproj", "{D83B27F3-4401-42F5-843E-147566B4999A}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowAsyncOperations", "examples\Workflow\WorkflowAsyncOperations\WorkflowAsyncOperations.csproj", "{00359961-0C50-4BB1-A794-8B06DE991639}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Messaging.Test", "test\Dapr.Messaging.Test\Dapr.Messaging.Test.csproj", "{4E04EB35-7FD2-4FDB-B09A-F75CE24053B9}" @@ -319,6 +321,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {D83B27F3-4401-42F5-843E-147566B4999A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D83B27F3-4401-42F5-843E-147566B4999A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D83B27F3-4401-42F5-843E-147566B4999A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D83B27F3-4401-42F5-843E-147566B4999A}.Release|Any CPU.Build.0 = Release|Any CPU {00359961-0C50-4BB1-A794-8B06DE991639}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {00359961-0C50-4BB1-A794-8B06DE991639}.Debug|Any CPU.Build.0 = Debug|Any CPU {00359961-0C50-4BB1-A794-8B06DE991639}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -403,6 +409,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} diff --git a/examples/Workflow/WorkflowFanOutFanIn/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowFanOutFanIn/Activities/NotifyActivity.cs new file mode 100644 index 000000000..88a5c7270 --- /dev/null +++ b/examples/Workflow/WorkflowFanOutFanIn/Activities/NotifyActivity.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowFanOutFanIn.Activities; + +internal sealed class NotifyActivity : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override Task RunAsync(WorkflowActivityContext context, string input) + { + Console.WriteLine(input); + return Task.FromResult(null); + } +} diff --git a/examples/Workflow/WorkflowFanOutFanIn/Program.cs b/examples/Workflow/WorkflowFanOutFanIn/Program.cs new file mode 100644 index 000000000..16d018ab0 --- /dev/null +++ b/examples/Workflow/WorkflowFanOutFanIn/Program.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 Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowFanOutFanIn.Activities; +using WorkflowFanOutFanIn.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + }); +}); + +var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +var instanceId = $"workflow-demo-{Guid.NewGuid().ToString()[..8]}"; +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, "test input"); + +await daprWorkflowClient.WaitForWorkflowCompletionAsync(instanceId); +var state = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); +Console.WriteLine($"Workflow state: {state.RuntimeStatus}"); diff --git a/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj b/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj new file mode 100644 index 000000000..af3a1b2c8 --- /dev/null +++ b/examples/Workflow/WorkflowFanOutFanIn/WorkflowFanOutFanIn.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowFanOutFanIn/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowFanOutFanIn/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..43a5b42b3 --- /dev/null +++ b/examples/Workflow/WorkflowFanOutFanIn/Workflows/DemoWorkflow.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 Dapr.Workflow; +using WorkflowFanOutFanIn.Activities; + +namespace WorkflowFanOutFanIn.Workflows; + +public sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, string input) + { + var tasks = new List(); + for (var a = 1; a <= 3; a++) + { + var task = context.CallActivityAsync(nameof(NotifyActivity), $"calling task {a}"); + tasks.Add(task); + } + + await Task.WhenAll(tasks); + + return "Workflow completed"; + } +} From 33d94a560c885f631f204830dcfc172a33cf1565 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 14 Nov 2024 10:04:49 -0700 Subject: [PATCH 36/69] Added workflow sample: Sub-workflows (#1395) * Added Workflow with sub-workflow Signed-off-by: Whit Waldo * Removed duplicate package version reference Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ++++ .../Workflow/WorkflowSubworkflow/Program.cs | 42 +++++++++++++++++++ .../WorkflowSubworkflow.csproj | 18 ++++++++ .../Workflows/DemoSubWorkflow.cs | 33 +++++++++++++++ .../Workflows/DemoWorkflow.cs | 34 +++++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 examples/Workflow/WorkflowSubworkflow/Program.cs create mode 100644 examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj create mode 100644 examples/Workflow/WorkflowSubworkflow/Workflows/DemoSubWorkflow.cs create mode 100644 examples/Workflow/WorkflowSubworkflow/Workflows/DemoWorkflow.cs diff --git a/all.sln b/all.sln index 81f41d4d6..26262176d 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowSubworkflow", "examples\Workflow\WorkflowSubworkflow\WorkflowSubworkflow.csproj", "{FD3E9371-3134-4235-8E80-32226DFB4B1F}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowFanOutFanIn", "examples\Workflow\WorkflowFanOutFanIn\WorkflowFanOutFanIn.csproj", "{D83B27F3-4401-42F5-843E-147566B4999A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowAsyncOperations", "examples\Workflow\WorkflowAsyncOperations\WorkflowAsyncOperations.csproj", "{00359961-0C50-4BB1-A794-8B06DE991639}" @@ -321,6 +323,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Release|Any CPU.Build.0 = Release|Any CPU {D83B27F3-4401-42F5-843E-147566B4999A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D83B27F3-4401-42F5-843E-147566B4999A}.Debug|Any CPU.Build.0 = Debug|Any CPU {D83B27F3-4401-42F5-843E-147566B4999A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -409,6 +415,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} diff --git a/examples/Workflow/WorkflowSubworkflow/Program.cs b/examples/Workflow/WorkflowSubworkflow/Program.cs new file mode 100644 index 000000000..b3e71537f --- /dev/null +++ b/examples/Workflow/WorkflowSubworkflow/Program.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowSubWorkflow.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterWorkflow(); + }); +}); + +using var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +var instanceId = $"demo-workflow-{Guid.NewGuid().ToString()[..8]}"; +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, instanceId ); + +await daprWorkflowClient.WaitForWorkflowCompletionAsync(instanceId); +var state = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); +Console.WriteLine($"Workflow {instanceId}, state: {state.RuntimeStatus}"); + +state = await daprWorkflowClient.GetWorkflowStateAsync($"{instanceId}-sub"); +Console.WriteLine($"Workflow {instanceId}-sub, state: {state.RuntimeStatus}"); diff --git a/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj b/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj new file mode 100644 index 000000000..af3a1b2c8 --- /dev/null +++ b/examples/Workflow/WorkflowSubworkflow/WorkflowSubworkflow.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowSubworkflow/Workflows/DemoSubWorkflow.cs b/examples/Workflow/WorkflowSubworkflow/Workflows/DemoSubWorkflow.cs new file mode 100644 index 000000000..9d9c0266c --- /dev/null +++ b/examples/Workflow/WorkflowSubworkflow/Workflows/DemoSubWorkflow.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowSubWorkflow.Workflows; + +internal sealed class DemoSubWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, string instanceId) + { + Console.WriteLine($"Workflow {context.InstanceId} started"); + Console.WriteLine($"Received input: {instanceId}"); + await context.CreateTimer(TimeSpan.FromSeconds(5)); + return true; + } +} diff --git a/examples/Workflow/WorkflowSubworkflow/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowSubworkflow/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..9fb0d6357 --- /dev/null +++ b/examples/Workflow/WorkflowSubworkflow/Workflows/DemoWorkflow.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowSubWorkflow.Workflows; + +internal sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, string instanceId) + { + Console.WriteLine($"Workflow {instanceId} started"); + var subInstanceId = instanceId + "-sub"; + var options = new ChildWorkflowTaskOptions(subInstanceId); + await context.CallChildWorkflowAsync(nameof(DemoSubWorkflow), "Hello, sub-workflow", options); + return true; + } +} From e04af4e89b02ad3b6d39e37e7c894eca5580818c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 14 Nov 2024 10:26:25 -0700 Subject: [PATCH 37/69] Added workflow sample: Task chaining (#1387) * Added Workflow Task Chaining example to replace https://github.com/dapr/dotnet-sdk/pull/1206 Signed-off-by: Whit Waldo * Targeting .NET 6, fixed transposition error Signed-off-by: Whit Waldo * Added missing copyright headers Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ++ .../WorkflowTaskChaining/Activities/Step1.cs | 31 +++++++++ .../WorkflowTaskChaining/Activities/Step2.cs | 31 +++++++++ .../WorkflowTaskChaining/Activities/Step3.cs | 31 +++++++++ .../Workflow/WorkflowTaskChaining/Program.cs | 64 +++++++++++++++++++ .../WorkflowTaskChaining.csproj | 18 ++++++ .../Workflows/DemoWorkflow.cs | 36 +++++++++++ test/Dapr.E2E.Test/DaprCommand.cs | 2 +- 8 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 examples/Workflow/WorkflowTaskChaining/Activities/Step1.cs create mode 100644 examples/Workflow/WorkflowTaskChaining/Activities/Step2.cs create mode 100644 examples/Workflow/WorkflowTaskChaining/Activities/Step3.cs create mode 100644 examples/Workflow/WorkflowTaskChaining/Program.cs create mode 100644 examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj create mode 100644 examples/Workflow/WorkflowTaskChaining/Workflows/DemoWorkflow.cs diff --git a/all.sln b/all.sln index 26262176d..fe0140528 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowTaskChaining", "examples\Workflow\WorkflowTaskChaining\WorkflowTaskChaining.csproj", "{945DD3B7-94E5-435E-B3CB-796C20A652C7}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowSubworkflow", "examples\Workflow\WorkflowSubworkflow\WorkflowSubworkflow.csproj", "{FD3E9371-3134-4235-8E80-32226DFB4B1F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowFanOutFanIn", "examples\Workflow\WorkflowFanOutFanIn\WorkflowFanOutFanIn.csproj", "{D83B27F3-4401-42F5-843E-147566B4999A}" @@ -323,6 +325,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Release|Any CPU.Build.0 = Release|Any CPU {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD3E9371-3134-4235-8E80-32226DFB4B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -415,6 +421,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} diff --git a/examples/Workflow/WorkflowTaskChaining/Activities/Step1.cs b/examples/Workflow/WorkflowTaskChaining/Activities/Step1.cs new file mode 100644 index 000000000..a0e160b40 --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/Activities/Step1.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowTaskChaining.Activities; + +internal sealed class Step1 : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override Task RunAsync(WorkflowActivityContext context, int input) + { + Console.WriteLine($@"Step 1: Received input: {input}."); + return Task.FromResult(input + 1); + } +} diff --git a/examples/Workflow/WorkflowTaskChaining/Activities/Step2.cs b/examples/Workflow/WorkflowTaskChaining/Activities/Step2.cs new file mode 100644 index 000000000..598239931 --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/Activities/Step2.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowTaskChaining.Activities; + +internal sealed class Step2 : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override Task RunAsync(WorkflowActivityContext context, int input) + { + Console.WriteLine($@"Step 2: Received input: {input}."); + return Task.FromResult(input + 2); + } +} diff --git a/examples/Workflow/WorkflowTaskChaining/Activities/Step3.cs b/examples/Workflow/WorkflowTaskChaining/Activities/Step3.cs new file mode 100644 index 000000000..67a390018 --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/Activities/Step3.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowTaskChaining.Activities; + +internal sealed class Step3 : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override Task RunAsync(WorkflowActivityContext context, int input) + { + Console.WriteLine($@"Step 3: Received input: {input}."); + return Task.FromResult(input ^ 2); + } +} diff --git a/examples/Workflow/WorkflowTaskChaining/Program.cs b/examples/Workflow/WorkflowTaskChaining/Program.cs new file mode 100644 index 000000000..126eff605 --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/Program.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowTaskChaining.Activities; +using WorkflowTaskChaining.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + }); +}); + +// Start the app - this is the point where we connect to the Dapr sidecar to listen +// for workflow work-items to execute +using var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +//Check health +const int wfInput = 42; +Console.WriteLine(@"Workflow Started"); + +var instanceId = $"demo-workflow-{Guid.NewGuid().ToString()[..8]}"; + +//Start the workflow immediately +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, wfInput); + +//Get the status of the workflow +WorkflowState workflowState; +while (true) +{ + workflowState = await daprWorkflowClient.GetWorkflowStateAsync(instanceId, true); + Console.WriteLine($@"Workflow status: {workflowState.RuntimeStatus}"); + if (workflowState.IsWorkflowCompleted) + break; + + await Task.Delay(TimeSpan.FromSeconds(1)); +} + +//Display the result from the workflow +var result = string.Join(" ", workflowState.ReadOutputAs() ?? Array.Empty()); +Console.WriteLine($@"Workflow result: {result}"); + + diff --git a/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj b/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj new file mode 100644 index 000000000..91ded8afb --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/WorkflowTaskChaining.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowTaskChaining/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowTaskChaining/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..722114349 --- /dev/null +++ b/examples/Workflow/WorkflowTaskChaining/Workflows/DemoWorkflow.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using WorkflowTaskChaining.Activities; + +namespace WorkflowTaskChaining.Workflows; + +internal sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, int input) + { + var result1 = await context.CallActivityAsync(nameof(Step1), input); + var result2 = await context.CallActivityAsync(nameof(Step2), result1); + var result3 = await context.CallActivityAsync(nameof(Step3), result2); + var ret = new int[] { result1, result2, result3 }; + + return ret; + } +} diff --git a/test/Dapr.E2E.Test/DaprCommand.cs b/test/Dapr.E2E.Test/DaprCommand.cs index 768e81960..a692ec638 100644 --- a/test/Dapr.E2E.Test/DaprCommand.cs +++ b/test/Dapr.E2E.Test/DaprCommand.cs @@ -38,7 +38,7 @@ public DaprCommand(ITestOutputHelper output) public void Run() { - Console.WriteLine($"Running command: {this.Command}"); + Console.WriteLine($@"Running command: {this.Command}"); var escapedArgs = Command.Replace("\"", "\\\""); var process = new Process() { From a972d2f070e56582e9b372edf019f6a2db8de4ea Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 14 Nov 2024 11:48:38 -0700 Subject: [PATCH 38/69] Added workflow sample: Monitor (#1388) * Added workflow monitor Signed-off-by: Whit Waldo * Restore to original argument names Signed-off-by: Whit Waldo * Update to target .NET 6 Signed-off-by: Whit Waldo * Added missing copyright headers Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 +++ .../WorkflowMonitor/Activities/CheckStatus.cs | 31 ++++++++++ examples/Workflow/WorkflowMonitor/Program.cs | 39 +++++++++++++ .../WorkflowMonitor/WorkflowMonitor.csproj | 18 ++++++ .../WorkflowMonitor/Workflows/DemoWorkflow.cs | 57 +++++++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 examples/Workflow/WorkflowMonitor/Activities/CheckStatus.cs create mode 100644 examples/Workflow/WorkflowMonitor/Program.cs create mode 100644 examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj create mode 100644 examples/Workflow/WorkflowMonitor/Workflows/DemoWorkflow.cs diff --git a/all.sln b/all.sln index fe0140528..f76687235 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowTaskChaining", "examples\Workflow\WorkflowTaskChaining\WorkflowTaskChaining.csproj", "{945DD3B7-94E5-435E-B3CB-796C20A652C7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowSubworkflow", "examples\Workflow\WorkflowSubworkflow\WorkflowSubworkflow.csproj", "{FD3E9371-3134-4235-8E80-32226DFB4B1F}" @@ -325,6 +327,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Release|Any CPU.Build.0 = Release|Any CPU {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Debug|Any CPU.Build.0 = Debug|Any CPU {945DD3B7-94E5-435E-B3CB-796C20A652C7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -421,6 +427,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} diff --git a/examples/Workflow/WorkflowMonitor/Activities/CheckStatus.cs b/examples/Workflow/WorkflowMonitor/Activities/CheckStatus.cs new file mode 100644 index 000000000..99e5ba2b1 --- /dev/null +++ b/examples/Workflow/WorkflowMonitor/Activities/CheckStatus.cs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowMonitor.Activities; + +internal sealed class CheckStatus : WorkflowActivity +{ + private static List status = new List { "healthy", "unhealthy" }; + private Random random = new(); + + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override Task RunAsync(WorkflowActivityContext context, bool input) => + Task.FromResult(status[random.Next(status.Count)]); +} diff --git a/examples/Workflow/WorkflowMonitor/Program.cs b/examples/Workflow/WorkflowMonitor/Program.cs new file mode 100644 index 000000000..05697d480 --- /dev/null +++ b/examples/Workflow/WorkflowMonitor/Program.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowMonitor.Activities; +using WorkflowMonitor.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + }); +}); + +using var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +var instanceId = $"demo-workflow-{Guid.NewGuid().ToString()[..8]}"; +var isHealthy = true; +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, isHealthy); + +//We don't want to block on workflow completion as this workflow will never complete diff --git a/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj b/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj new file mode 100644 index 000000000..91ded8afb --- /dev/null +++ b/examples/Workflow/WorkflowMonitor/WorkflowMonitor.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowMonitor/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowMonitor/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..e456eea15 --- /dev/null +++ b/examples/Workflow/WorkflowMonitor/Workflows/DemoWorkflow.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using WorkflowMonitor.Activities; + +namespace WorkflowMonitor.Workflows; + +internal sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, bool isHealthy) + { + string status = await context.CallActivityAsync(nameof(CheckStatus), true); + int next_sleep_interval; + if (!context.IsReplaying) + { + Console.WriteLine($"This job is {status}"); + } + + if (status == "healthy") + { + isHealthy = true; + next_sleep_interval = 30; + } + else + { + if (isHealthy) + { + isHealthy = false; + } + Console.WriteLine("Status is unhealthy. Set check interval to 5s"); + next_sleep_interval = 5; + } + + await context.CreateTimer(TimeSpan.FromSeconds(next_sleep_interval)); + context.ContinueAsNew(isHealthy); + + //This workflow will never complete + return true; + } +} From f9c9e23e18eee71a32ca83720a07eae6745d1cb5 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 18 Nov 2024 11:48:53 -0700 Subject: [PATCH 39/69] Added workflow example: External interaction (#1389) * Added workflow example demonstrating external interaction Signed-off-by: Whit Waldo * Added copyright headers Signed-off-by: Whit Waldo * Fixed .sln file Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ++ .../Activities/ApproveActivity.cs | 33 ++++++++ .../Activities/RejectActivity.cs | 33 ++++++++ .../WorkflowExternalInteraction/Program.cs | 76 +++++++++++++++++++ .../WorkflowExternalInteraction.csproj | 18 +++++ .../Workflows/DemoWorkflow.cs | 46 +++++++++++ 6 files changed, 213 insertions(+) create mode 100644 examples/Workflow/WorkflowExternalInteraction/Activities/ApproveActivity.cs create mode 100644 examples/Workflow/WorkflowExternalInteraction/Activities/RejectActivity.cs create mode 100644 examples/Workflow/WorkflowExternalInteraction/Program.cs create mode 100644 examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj create mode 100644 examples/Workflow/WorkflowExternalInteraction/Workflows/DemoWorkflow.cs diff --git a/all.sln b/all.sln index f76687235..8a6eb2fff 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowExternalInteraction", "examples\Workflow\WorkflowExternalInteraction\WorkflowExternalInteraction.csproj", "{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowTaskChaining", "examples\Workflow\WorkflowTaskChaining\WorkflowTaskChaining.csproj", "{945DD3B7-94E5-435E-B3CB-796C20A652C7}" @@ -327,6 +329,10 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.Build.0 = Release|Any CPU {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -427,6 +433,7 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} diff --git a/examples/Workflow/WorkflowExternalInteraction/Activities/ApproveActivity.cs b/examples/Workflow/WorkflowExternalInteraction/Activities/ApproveActivity.cs new file mode 100644 index 000000000..48048e19f --- /dev/null +++ b/examples/Workflow/WorkflowExternalInteraction/Activities/ApproveActivity.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowExternalInteraction.Activities; + +internal sealed class ApproveActivity : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + Console.WriteLine($"Workflow {input} is approved"); + Console.WriteLine("Running Approval activity..."); + await Task.Delay(TimeSpan.FromSeconds(5)); + return true; + } +} diff --git a/examples/Workflow/WorkflowExternalInteraction/Activities/RejectActivity.cs b/examples/Workflow/WorkflowExternalInteraction/Activities/RejectActivity.cs new file mode 100644 index 000000000..765d31a27 --- /dev/null +++ b/examples/Workflow/WorkflowExternalInteraction/Activities/RejectActivity.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; + +namespace WorkflowExternalInteraction.Activities; + +internal sealed class RejectActivity : WorkflowActivity +{ + /// + /// Override to implement async (non-blocking) workflow activity logic. + /// + /// Provides access to additional context for the current activity execution. + /// The deserialized activity input. + /// The output of the activity as a task. + public override async Task RunAsync(WorkflowActivityContext context, string input) + { + Console.WriteLine($"Workflow {input} is rejected"); + Console.WriteLine("Running Reject activity..."); + await Task.Delay(TimeSpan.FromSeconds(5)); + return true; + } +} diff --git a/examples/Workflow/WorkflowExternalInteraction/Program.cs b/examples/Workflow/WorkflowExternalInteraction/Program.cs new file mode 100644 index 000000000..b83527d25 --- /dev/null +++ b/examples/Workflow/WorkflowExternalInteraction/Program.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WorkflowExternalInteraction.Activities; +using WorkflowExternalInteraction.Workflows; + +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => +{ + services.AddDaprWorkflow(options => + { + options.RegisterWorkflow(); + options.RegisterActivity(); + options.RegisterActivity(); + }); +}); + +using var host = builder.Build(); +await host.StartAsync(); + +await using var scope = host.Services.CreateAsyncScope(); +var daprWorkflowClient = scope.ServiceProvider.GetRequiredService(); + +var instanceId = $"demo-workflow-{Guid.NewGuid().ToString()[..8]}"; + +await daprWorkflowClient.ScheduleNewWorkflowAsync(nameof(DemoWorkflow), instanceId, instanceId); + + +bool enterPressed = false; +Console.WriteLine("Press [ENTER] within the next 10 seconds to approve this workflow"); +using (var cts = new CancellationTokenSource()) +{ + var inputTask = Task.Run(() => + { + if (Console.ReadKey().Key == ConsoleKey.Enter) + { + Console.WriteLine("Approved"); + enterPressed = true; + cts.Cancel(); //Cancel the delay task if Enter is pressed + } + }); + + try + { + await Task.Delay(TimeSpan.FromSeconds(10), cts.Token); + } + catch (TaskCanceledException) + { + // Task was cancelled because Enter was pressed + } +} + +if (enterPressed) +{ + await daprWorkflowClient.RaiseEventAsync(instanceId, "Approval", true); +} +else +{ + Console.WriteLine("Rejected"); +} + +await daprWorkflowClient.WaitForWorkflowCompletionAsync(instanceId); +var state = await daprWorkflowClient.GetWorkflowStateAsync(instanceId); +Console.WriteLine($"Workflow state: {state.RuntimeStatus}"); diff --git a/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj b/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj new file mode 100644 index 000000000..4aae25c46 --- /dev/null +++ b/examples/Workflow/WorkflowExternalInteraction/WorkflowExternalInteraction.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/Workflow/WorkflowExternalInteraction/Workflows/DemoWorkflow.cs b/examples/Workflow/WorkflowExternalInteraction/Workflows/DemoWorkflow.cs new file mode 100644 index 000000000..ba815a800 --- /dev/null +++ b/examples/Workflow/WorkflowExternalInteraction/Workflows/DemoWorkflow.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Workflow; +using WorkflowExternalInteraction.Activities; + +namespace WorkflowExternalInteraction.Workflows; + +internal sealed class DemoWorkflow : Workflow +{ + /// + /// Override to implement workflow logic. + /// + /// The workflow context. + /// The deserialized workflow input. + /// The output of the workflow as a task. + public override async Task RunAsync(WorkflowContext context, string input) + { + try + { + await context.WaitForExternalEventAsync(eventName: "Approval", timeout: TimeSpan.FromSeconds(10)); + } + catch (TaskCanceledException) + { + Console.WriteLine("Approval timeout"); + await context.CallActivityAsync(nameof(RejectActivity), input); + Console.WriteLine("Reject Activity finished"); + return false; + } + + await context.CallActivityAsync(nameof(ApproveActivity), input); + Console.WriteLine("Approve Activity finished"); + + return true; + } +} From 105f7985183c532b84571a6f3c3f2ee5b93cba1f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 21 Nov 2024 14:40:20 -0700 Subject: [PATCH 40/69] Optional DI lifecycle change (#1408) * Added mechanism to allow the service lifetime to be overridden from a singleton (default) to another lifetime Signed-off-by: Whit Waldo * Added unit tests - updated dependencies accordingly Signed-off-by: Whit Waldo * Added service lifetime to DaprClient as well Signed-off-by: Whit Waldo * Added update to DaprClient to pass service lifetime through Signed-off-by: Whit Waldo * Added documentation indicating how to register DaprWorkflowClient with different lifecycle options. Signed-off-by: Whit Waldo * Removed unnecessary line from csproj Signed-off-by: Whit Waldo * Simplified registrations Signed-off-by: Whit Waldo * Called out an important point about registrations Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 4 +- all.sln | 7 ++ .../dotnet-workflowclient-usage.md | 77 +++++++++++++++++++ .../DaprServiceCollectionExtensions.cs | 41 ++++++++-- src/Dapr.Workflow/Dapr.Workflow.csproj | 1 - .../WorkflowServiceCollectionExtensions.cs | 39 ++++++++-- .../Dapr.Workflow.Test.csproj | 32 ++++---- ...orkflowServiceCollectionExtensionsTests.cs | 58 ++++++++++++++ 8 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md create mode 100644 test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 772dd7c6e..a98e9db58 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,8 +28,8 @@ - - + + diff --git a/all.sln b/all.sln index 8a6eb2fff..bb44a3bdc 100644 --- a/all.sln +++ b/all.sln @@ -145,6 +145,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{D9697361-2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -377,6 +379,10 @@ Global {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.Build.0 = Debug|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.ActiveCfg = Release|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.Build.0 = Release|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -446,6 +452,7 @@ Global {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md new file mode 100644 index 000000000..ac6a0f189 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -0,0 +1,77 @@ +--- +type: docs +title: "DaprWorkflowClient usage" +linkTitle: "DaprWorkflowClient usage" +weight: 100000 +description: Essential tips and advice for using DaprWorkflowClient +--- + +## Lifetime management + +A `DaprWorkflowClient` holds access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar as well +as other types used in the management and operation of Workflows. `DaprWorkflowClient` implements `IAsyncDisposable` to support eager +cleanup of resources. + +## Dependency Injection + +The `AddDaprWorkflow()` method will register the Dapr workflow services with ASP.NET Core dependency injection. This method +requires an options delegate that defines each of the workflows and activities you wish to register and use in your application. + +{{% alert title="Note" color="primary" %}} + +This method will attempt to register a `DaprClient` instance, but this will only work if it hasn't already been registered with another +lifetime. For example, an earlier call to `AddDaprClient()` with a singleton lifetime will always use a singleton regardless of the +lifetime chose for the workflow client. The `DaprClient` instance will be used to communicate with the Dapr sidecar and if it's not +yet registered, the lifetime provided during the `AddDaprWorkflow()` registration will be used to register the `DaprWorkflowClient` +as well as its own dependencies. + +{{% /alert %}} + +### Singleton Registration +By default, the `AddDaprWorkflow` method will register the `DaprWorkflowClient` and associated services using a singleton lifetime. This means +that the services will be instantiated only a single time. + +The following is an example of how registration of the `DaprWorkflowClient` as it would appear in a typical `Program.cs` file: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}); + +var app = builder.Build(); +await app.RunAsync(); +``` + +### Scoped Registration + +While this may generally be acceptable in your use case, you may instead wish to override the lifetime specified. This is done by passing a `ServiceLifetime` +argument in `AddDaprWorkflow`. For example, you may wish to inject another scoped service into your ASP.NET Core processing pipeline +that needs context used by the `DaprClient` that wouldn't be available if the former service were registered as a singleton. + +This is demonstrated in the following example: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}, ServiceLifecycle.Scoped); + +var app = builder.Build(); +await app.RunAsync(); +``` + +### Transient Registration + +Finally, Dapr services can also be registered using a transient lifetime meaning that they will be initialized every time they're injected. This +is demonstrated in the following example: + +```csharp +builder.Services.AddDaprWorkflow(options => { + options.RegisterWorkflow(); + options.RegisterActivity(); +}, ServiceLifecycle.Transient); + +var app = builder.Build(); +await app.RunAsync(); +``` \ No newline at end of file diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 52e9110be..ea6fb520e 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -32,16 +32,32 @@ public static class DaprServiceCollectionExtensions /// /// The . /// - public static void AddDaprClient(this IServiceCollection services, Action? configure = null) + /// The lifetime of the registered services. + public static void AddDaprClient(this IServiceCollection services, Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - services.TryAddSingleton(serviceProvider => + var registration = new Func((serviceProvider) => { var builder = CreateDaprClientBuilder(serviceProvider); configure?.Invoke(builder); return builder.Build(); }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + services.TryAddSingleton(registration); + break; + } } /// @@ -50,17 +66,32 @@ public static void AddDaprClient(this IServiceCollection services, Action /// The . /// + /// The lifetime of the registered services. public static void AddDaprClient(this IServiceCollection services, - Action configure) + Action configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - - services.TryAddSingleton(serviceProvider => + + var registration = new Func((serviceProvider) => { var builder = CreateDaprClientBuilder(serviceProvider); configure?.Invoke(serviceProvider, builder); return builder.Build(); }); + + switch (lifetime) + { + case ServiceLifetime.Singleton: + services.TryAddSingleton(registration); + break; + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + default: + services.TryAddTransient(registration); + break; + } } private static DaprClientBuilder CreateDaprClientBuilder(IServiceProvider serviceProvider) diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 992baee73..360d121ef 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -3,7 +3,6 @@ - net6;net7;net8 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 5c10a776e..209e4edc0 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -29,25 +29,48 @@ public static class WorkflowServiceCollectionExtensions /// /// The . /// A delegate used to configure actor options and register workflow functions. + /// The lifetime of the registered services. public static IServiceCollection AddDaprWorkflow( this IServiceCollection serviceCollection, - Action configure) + Action configure, + ServiceLifetime lifetime = ServiceLifetime.Singleton) { if (serviceCollection == null) { throw new ArgumentNullException(nameof(serviceCollection)); } - serviceCollection.TryAddSingleton(); + serviceCollection.AddDaprClient(lifetime: lifetime); serviceCollection.AddHttpClient(); - + serviceCollection.AddHostedService(); + + switch (lifetime) + { + case ServiceLifetime.Singleton: #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); #pragma warning restore CS0618 // Type or member is obsolete - serviceCollection.AddHostedService(); - serviceCollection.TryAddSingleton(); - serviceCollection.AddDaprClient(); - + serviceCollection.TryAddSingleton(); + serviceCollection.TryAddSingleton(); + break; + case ServiceLifetime.Scoped: +#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient + serviceCollection.TryAddScoped(); +#pragma warning restore CS0618 // Type or member is obsolete + serviceCollection.TryAddScoped(); + serviceCollection.TryAddScoped(); + break; + case ServiceLifetime.Transient: +#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient + serviceCollection.TryAddTransient(); +#pragma warning restore CS0618 // Type or member is obsolete + serviceCollection.TryAddTransient(); + serviceCollection.TryAddTransient(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null); + } + serviceCollection.AddOptions().Configure(configure); //Register the factory and force resolution so the Durable Task client and worker can be registered diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj index 15e626772..531a0b1f1 100644 --- a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -1,25 +1,27 @@ + - Dapr.Workflow.Tests + enable + enable + false + true - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + - + + + + + + diff --git a/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..2206d939b --- /dev/null +++ b/test/Dapr.Workflow.Test/WorkflowServiceCollectionExtensionsTests.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Workflow.Test; + +public class WorkflowServiceCollectionExtensionsTests +{ + [Fact] + public void RegisterWorkflowClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprWorkflowClient1 = serviceProvider.GetService(); + var daprWorkflowClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + + Assert.Same(daprWorkflowClient1, daprWorkflowClient2); + } + + [Fact] + public async Task RegisterWorkflowClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprWorkflowClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprWorkflowClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2); + } + + [Fact] + public void RegisterWorkflowClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprWorkflow(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprWorkflowClient1 = serviceProvider.GetService(); + var daprWorkflowClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprWorkflowClient1); + Assert.NotNull(daprWorkflowClient2); + Assert.NotSame(daprWorkflowClient1, daprWorkflowClient2); + } +} From 7933066d697ceecdc65c0adc935f10e2e682c4d7 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 24 Nov 2024 01:14:10 -0700 Subject: [PATCH 41/69] Additional lifecycle registration changes (#1410) * Added service lifetime to Jobs client Signed-off-by: Whit Waldo * Added service lifetime to messaging client Signed-off-by: Whit Waldo * Added service lifetime to actors registration Signed-off-by: Whit Waldo * Added unit tests for DaprClient Signed-off-by: Whit Waldo * Minor naming tweaks Signed-off-by: Whit Waldo * Removed invalid using Signed-off-by: Whit Waldo * Added service lifetime tests for actors Signed-off-by: Whit Waldo * Added unit tests for jobs client lifecycle registrations Signed-off-by: Whit Waldo * Added unit tests for PubSub and lifecycle registration Signed-off-by: Whit Waldo * Fixed missing registration dependency Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../ActorsServiceCollectionExtensions.cs | 38 +++-- .../DaprJobsServiceCollectionExtensions.cs | 24 ++- ...ishSubscribeServiceCollectionExtensions.cs | 19 ++- ...aprActorServiceCollectionExtensionsTest.cs | 60 ++++++++ .../DaprServiceCollectionExtensionsTest.cs | 142 ++++++++++++------ ...aprJobsServiceCollectionExtensionsTests.cs | 54 ++++++- ...bscribeServiceCollectionExtensionsTests.cs | 76 ++++++++++ 7 files changed, 352 insertions(+), 61 deletions(-) create mode 100644 test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs create mode 100644 test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs diff --git a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs index 11f05f4c1..9b80975db 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsServiceCollectionExtensions.cs @@ -34,17 +34,18 @@ public static class ActorsServiceCollectionExtensions /// /// The . /// A delegate used to configure actor options and register actor types. - public static void AddActors(this IServiceCollection? services, Action? configure) + /// The lifetime of the registered services. + public static void AddActors(this IServiceCollection? services, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); - // Routing and health checks are required dependencies. + // Routing, health checks and logging are required dependencies. services.AddRouting(); services.AddHealthChecks(); + services.AddLogging(); - services.TryAddSingleton(); - services.TryAddSingleton(s => - { + var actorRuntimeRegistration = new Func(s => + { var options = s.GetRequiredService>().Value; ConfigureActorOptions(s, options); @@ -53,11 +54,10 @@ public static void AddActors(this IServiceCollection? services, Action(); return new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); }); - - services.TryAddSingleton(s => + var proxyFactoryRegistration = new Func(serviceProvider => { - var options = s.GetRequiredService>().Value; - ConfigureActorOptions(s, options); + var options = serviceProvider.GetRequiredService>().Value; + ConfigureActorOptions(serviceProvider, options); var factory = new ActorProxyFactory() { @@ -72,6 +72,26 @@ public static void AddActors(this IServiceCollection? services, Action(); + services.TryAddScoped(actorRuntimeRegistration); + services.TryAddScoped(proxyFactoryRegistration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(); + services.TryAddTransient(actorRuntimeRegistration); + services.TryAddTransient(proxyFactoryRegistration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(); + services.TryAddSingleton(actorRuntimeRegistration); + services.TryAddSingleton(proxyFactoryRegistration); + break; + } + if (configure != null) { services.Configure(configure); diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 67e718985..93265837b 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -26,25 +26,40 @@ public static class DaprJobsServiceCollectionExtensions ///
/// The . /// Optionally allows greater configuration of the . - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + /// The lifetime of the registered services. + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); //Register the IHttpClientFactory implementation serviceCollection.AddHttpClient(); - serviceCollection.TryAddSingleton(serviceProvider => + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); var builder = new DaprJobsClientBuilder(); builder.UseHttpClientFactory(httpClientFactory); - + configure?.Invoke(builder); return builder.Build(); }); + switch (lifetime) + { + case ServiceLifetime.Scoped: + serviceCollection.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + serviceCollection.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + serviceCollection.TryAddSingleton(registration); + break; + } + return serviceCollection; } @@ -53,8 +68,9 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi ///
/// The . /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index bc60c5880..fe9b7c417 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -13,15 +13,16 @@ public static class PublishSubscribeServiceCollectionExtensions ///
/// The . /// Optionally allows greater configuration of the using injected services. + /// The lifetime of the registered services. /// - public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null) + public static IServiceCollection AddDaprPubSubClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); //Register the IHttpClientFactory implementation services.AddHttpClient(); - services.TryAddSingleton(serviceProvider => + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); @@ -33,6 +34,20 @@ public static IServiceCollection AddDaprPubSubClient(this IServiceCollection ser return builder.Build(); }); + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(registration); + break; + } + return services; } } diff --git a/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs b/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs new file mode 100644 index 000000000..3255fb785 --- /dev/null +++ b/test/Dapr.Actors.AspNetCore.Test/DaprActorServiceCollectionExtensionsTest.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Actors.AspNetCore.Test; + +public sealed class DaprActorServiceCollectionExtensionsTest +{ + [Fact] + public void RegisterActorsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + + Assert.Same(daprClient1, daprClient2); + } + + [Fact] + public async Task RegisterActorsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } + + [Fact] + public void RegisterActorsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddActors(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } +} diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 4a340e22a..2028a9fbb 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -15,67 +15,120 @@ using System; using System.Text.Json; +using System.Threading.Tasks; using Dapr.Client; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Dapr.AspNetCore.Test +namespace Dapr.AspNetCore.Test; + +public class DaprServiceCollectionExtensionsTest { - public class DaprServiceCollectionExtensionsTest + [Fact] + public void AddDaprClient_RegistersDaprClientOnlyOnce() { - [Fact] - public void AddDaprClient_RegistersDaprClientOnlyOnce() - { - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var clientBuilder = new Action( - builder => builder.UseJsonSerializationOptions( - new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = false - } - ) - ); + var clientBuilder = new Action( + builder => builder.UseJsonSerializationOptions( + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = false + } + ) + ); - // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) - services.AddDaprClient(); + // register with JsonSerializerOptions.PropertyNameCaseInsensitive = true (default) + services.AddDaprClient(); - // register with PropertyNameCaseInsensitive = false - services.AddDaprClient(clientBuilder); + // register with PropertyNameCaseInsensitive = false + services.AddDaprClient(clientBuilder); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; + DaprClientGrpc? daprClient = serviceProvider.GetService() as DaprClientGrpc; - Assert.NotNull(daprClient); - Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + Assert.NotNull(daprClient); + Assert.True(daprClient?.JsonSerializerOptions.PropertyNameCaseInsensitive); + } - [Fact] - public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + [Fact] + public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + { + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprClient((provider, builder) => { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddDaprClient((provider, builder) => + builder.UseJsonSerializationOptions(new JsonSerializerOptions { - var configProvider = provider.GetRequiredService(); - var caseSensitivity = configProvider.GetCaseSensitivity(); - - builder.UseJsonSerializationOptions(new JsonSerializerOptions - { - PropertyNameCaseInsensitive = caseSensitivity - }); + PropertyNameCaseInsensitive = caseSensitivity }); + }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - DaprClientGrpc? client = serviceProvider.GetRequiredService() as DaprClientGrpc; + DaprClientGrpc? client = serviceProvider.GetRequiredService() as DaprClientGrpc; - //Registers with case-insensitive as true by default, but we set as false above - Assert.NotNull(client); - Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); - } + //Registers with case-insensitive as true by default, but we set as false above + Assert.NotNull(client); + Assert.False(client?.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void RegisterClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + + Assert.Same(daprClient1, daprClient2); + } + + [Fact] + public async Task RegisterDaprClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } + + [Fact] + public void RegisterDaprClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprClient(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprClient1 = serviceProvider.GetService(); + var daprClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprClient1); + Assert.NotNull(daprClient2); + Assert.NotSame(daprClient1, daprClient2); + } #if NET8_0_OR_GREATER @@ -96,9 +149,8 @@ public void AddDaprClient_WithKeyedServices() } #endif - private class TestConfigurationProvider - { - public bool GetCaseSensitivity() => false; - } + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; } } diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 34d900aeb..281477d4e 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -13,9 +13,9 @@ using System; using System.Net.Http; +using System.Threading.Tasks; using Dapr.Jobs.Extensions; using Microsoft.Extensions.DependencyInjection; -using Xunit; namespace Dapr.Jobs.Test.Extensions; @@ -77,6 +77,58 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); } + + [Fact] + public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprJobsClient1 = serviceProvider.GetService(); + var daprJobsClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + + Assert.Same(daprJobsClient1, daprJobsClient2); + } + + [Fact] + public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprJobsClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprJobsClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + Assert.NotSame(daprJobsClient1, daprJobsClient2); + } + + [Fact] + public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprJobsClient1 = serviceProvider.GetService(); + var daprJobsClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprJobsClient1); + Assert.NotNull(daprJobsClient2); + Assert.NotSame(daprJobsClient1, daprJobsClient2); + } private class TestSecretRetriever { diff --git a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..d239fb86d --- /dev/null +++ b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs @@ -0,0 +1,76 @@ +using Dapr.Messaging.PublishSubscribe; +using Dapr.Messaging.PublishSubscribe.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Messaging.Test.Extensions; + +public sealed class PublishSubscribeServiceCollectionExtensionsTests +{ + [Fact] + public void AddDaprPubSubClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprPubSubClient = serviceProvider.GetService(); + Assert.NotNull(daprPubSubClient); + } + + [Fact] + public void RegisterPubsubClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprPubSubClient1 = serviceProvider.GetService(); + var daprPubSubClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + + Assert.Same(daprPubSubClient1, daprPubSubClient2); + } + + [Fact] + public async Task RegisterPubsubClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprPubSubClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprPubSubClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + Assert.NotSame(daprPubSubClient1, daprPubSubClient2); + } + + [Fact] + public void RegisterPubsubClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprPubSubClient(lifetime: ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprPubSubClient1 = serviceProvider.GetService(); + var daprPubSubClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprPubSubClient1); + Assert.NotNull(daprPubSubClient2); + Assert.NotSame(daprPubSubClient1, daprPubSubClient2); + } +} From 072c0603343182d98ea1e7ffed39d19d3f2d3baf Mon Sep 17 00:00:00 2001 From: Tomas Hrebicek Date: Sat, 30 Nov 2024 06:16:18 +0100 Subject: [PATCH 42/69] Preserve comparer of the original dictionary from ConfigurationProvider (#935) Signed-off-by: Tomas Hrebicek Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../DaprConfigurationStoreProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs index ea00f6c43..461ee9959 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs @@ -89,7 +89,7 @@ private async Task LoadAsync() var subscribeConfigurationResponse = await daprClient.SubscribeConfiguration(store, keys, metadata, cts.Token); await foreach (var items in subscribeConfigurationResponse.Source.WithCancellation(cts.Token)) { - var data = new Dictionary(Data); + var data = new Dictionary(Data, StringComparer.OrdinalIgnoreCase); foreach (var item in items) { id = subscribeConfigurationResponse.Id; From 4ed348078021f74e7f059f75e1f131b403eb5526 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 29 Nov 2024 23:24:41 -0600 Subject: [PATCH 43/69] Update all.sln Removed duplicate project include of Dapr.Workflow.Test Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- all.sln | 7 ------- 1 file changed, 7 deletions(-) diff --git a/all.sln b/all.sln index bb44a3bdc..8a6eb2fff 100644 --- a/all.sln +++ b/all.sln @@ -145,8 +145,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{D9697361-2 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -379,10 +377,6 @@ Global {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Debug|Any CPU.Build.0 = Debug|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.ActiveCfg = Release|Any CPU {9CAF360E-5AD3-4C4F-89A0-327EEB70D673}.Release|Any CPU.Build.0 = Release|Any CPU - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -452,7 +446,6 @@ Global {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} From ed68f285bf693c10b5ec4d94c9f01a077d8df48a Mon Sep 17 00:00:00 2001 From: paule96 Date: Tue, 3 Dec 2024 21:26:00 +0100 Subject: [PATCH 44/69] =?UTF-8?q?Bug/476=20multiple=20methods=20per=20inte?= =?UTF-8?q?rface=20with=20JSON=20serialization=20doesn=C2=B4t=20work=20(#1?= =?UTF-8?q?343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update devcontainer Signed-off-by: paule96 * update test setup Signed-off-by: paule96 * Now the json serialization should work with multiple methods in an interface Signed-off-by: paule96 * fixed devcontainer to run actors Now the devcontainer uses docker in docker, so you can reach the dapr setup after you did run dapr init. This will then only affect the dev container, without compromising the host of the devcontainer Signed-off-by: paule96 * fix bugs with the current implementation Signed-off-by: paule96 * add a test that checks excatly the behavior Signed-off-by: paule96 * fix devcontainer post creatd command Signed-off-by: paule96 * change the default to dotnet 8.0 Signed-off-by: paule96 * I don't know what is different but we commit. Maybe it resolves the need of chmod for it 🤷‍♀️ Signed-off-by: paule96 * make it easier to see why the application of an E2E test couldn't start Signed-off-by: paule96 * make the exception in E2E more percise Signed-off-by: paule96 * fix exception message Signed-off-by: paule96 --------- Signed-off-by: paule96 Co-authored-by: Yaron Schneider Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .devcontainer/devcontainer.json | 20 +++-- .devcontainer/localinit.sh | 0 ...torMessageBodyJsonSerializationProvider.cs | 5 +- .../ActorMessageSerializersManager.cs | 44 +++++++--- src/Dapr.Actors/DaprHttpInteractor.cs | 4 +- src/Dapr.Actors/Runtime/ActorManager.cs | 10 +-- .../ISerializationActor.cs | 2 + .../Actors/SerializationActor.cs | 5 ++ .../Actors/E2ETests.CustomSerializerTests.cs | 27 ++++++ test/Dapr.E2E.Test/DaprCommand.cs | 85 +++++++++++++++++-- 10 files changed, 170 insertions(+), 32 deletions(-) mode change 100644 => 100755 .devcontainer/localinit.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e179c66b2..3658b56ba 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -10,11 +10,15 @@ "ghcr.io/devcontainers/features/azure-cli:1": { "version": "2.38" }, - "ghcr.io/devcontainers/features/docker-from-docker:1": { - "version": "20.10" + "ghcr.io/devcontainers/features/docker-in-docker": { + "version": "latest" }, - "ghcr.io/devcontainers/features/dotnet:1": { - "version": "6.0" + "ghcr.io/devcontainers/features/dotnet": { + "version": "8.0", + "additionalVersions": [ + "6.0", + "7.0" + ] }, "ghcr.io/devcontainers/features/github-cli:1": { "version": "2" @@ -32,7 +36,8 @@ "ms-dotnettools.csharp", "ms-dotnettools.vscode-dotnet-runtime", "ms-azuretools.vscode-dapr", - "GitHub.copilot" + "GitHub.copilot", + "ms-dotnettools.csdevkit" ], "forwardPorts": [ 3000, @@ -42,10 +47,9 @@ 5000, 5007 ], - "postCreateCommand": ".devcontainer/localinit.sh", + "postCreateCommand": "chmod +x .devcontainer/localinit.sh && .devcontainer/localinit.sh", "remoteUser": "vscode", "hostRequirements": { "memory": "8gb" } - } - \ No newline at end of file +} \ No newline at end of file diff --git a/.devcontainer/localinit.sh b/.devcontainer/localinit.sh old mode 100644 new mode 100755 diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs index 062d3c742..88bc11cef 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs @@ -103,7 +103,10 @@ public MemoryStreamMessageBodySerializer( { var _methodRequestParameterTypes = new List(methodRequestParameterTypes); var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes); - + if(_wrappedRequestMessageTypes.Count > 1){ + throw new NotSupportedException("JSON serialisation should always provide the actor method (or nothing), that was called" + + " to support (de)serialisation. This is a Dapr SDK error, open an issue on GitHub."); + } this.serializerOptions = new(serializerOptions) { // Workaround since WrappedMessageBody creates an object diff --git a/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs b/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs index fb68e4cf2..3355aff1a 100644 --- a/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs +++ b/src/Dapr.Actors/Communication/ActorMessageSerializersManager.cs @@ -15,11 +15,14 @@ namespace Dapr.Actors.Communication { using System; using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; using Dapr.Actors.Builder; internal class ActorMessageSerializersManager { - private readonly ConcurrentDictionary cachedBodySerializers; + private readonly ConcurrentDictionary<(int, string), CacheEntry> cachedBodySerializers; private readonly IActorMessageHeaderSerializer headerSerializer; private readonly IActorMessageBodySerializationProvider serializationProvider; @@ -38,7 +41,7 @@ public ActorMessageSerializersManager( } this.serializationProvider = serializationProvider; - this.cachedBodySerializers = new ConcurrentDictionary(); + this.cachedBodySerializers = new ConcurrentDictionary<(int, string), CacheEntry>(); this.headerSerializer = headerSerializer; } @@ -52,19 +55,19 @@ public IActorMessageHeaderSerializer GetHeaderSerializer() return this.headerSerializer; } - public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId) + public IActorRequestMessageBodySerializer GetRequestMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) { - return this.cachedBodySerializers.GetOrAdd(interfaceId, this.CreateSerializers).RequestMessageBodySerializer; + return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).RequestMessageBodySerializer; } - public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId) + public IActorResponseMessageBodySerializer GetResponseMessageBodySerializer(int interfaceId, [AllowNull] string methodName = null) { - return this.cachedBodySerializers.GetOrAdd(interfaceId, this.CreateSerializers).ResponseMessageBodySerializer; + return this.cachedBodySerializers.GetOrAdd((interfaceId, methodName), this.CreateSerializers).ResponseMessageBodySerializer; } - internal CacheEntry CreateSerializers(int interfaceId) + internal CacheEntry CreateSerializers((int interfaceId, string methodName) data) { - var interfaceDetails = this.GetInterfaceDetails(interfaceId); + var interfaceDetails = this.GetInterfaceDetails(data.interfaceId); // get the service interface type from the code gen layer var serviceInterfaceType = interfaceDetails.ServiceInterfaceType; @@ -74,10 +77,29 @@ internal CacheEntry CreateSerializers(int interfaceId) // get the known types from the codegen layer var responseBodyTypes = interfaceDetails.ResponseKnownTypes; + if (data.methodName is null) + { + // Path is mainly used for XML serialization + return new CacheEntry( + this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes), + this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes)); + } + else + { + // This path should be used for JSON serialization + var requestWrapperTypeAsList = interfaceDetails.RequestWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}ReqBody").ToList(); + if(requestWrapperTypeAsList.Count > 1){ + throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); + } + var responseWrapperTypeAsList = interfaceDetails.ResponseWrappedKnownTypes.Where(r => r.Name == $"{data.methodName}RespBody").ToList(); + if(responseWrapperTypeAsList.Count > 1){ + throw new NotSupportedException($"More then one wrappertype was found for {data.methodName}"); + } + return new CacheEntry( + this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, requestWrapperTypeAsList), + this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, responseWrapperTypeAsList)); + } - return new CacheEntry( - this.serializationProvider.CreateRequestMessageBodySerializer(serviceInterfaceType, requestBodyTypes, interfaceDetails.RequestWrappedKnownTypes), - this.serializationProvider.CreateResponseMessageBodySerializer(serviceInterfaceType, responseBodyTypes, interfaceDetails.ResponseWrappedKnownTypes)); } internal InterfaceDetails GetInterfaceDetails(int interfaceId) diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index 2565bab62..4643ad2f4 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -116,7 +116,7 @@ public async Task InvokeActorMethodWithRemotingAsync(Acto var serializedHeader = serializersManager.GetHeaderSerializer() .SerializeRequestHeader(remotingRequestRequestMessage.GetHeader()); - var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId); + var msgBodySeriaizer = serializersManager.GetRequestMessageBodySerializer(interfaceId, methodName); var serializedMsgBody = msgBodySeriaizer.Serialize(remotingRequestRequestMessage.GetBody()); // Send Request @@ -170,7 +170,7 @@ HttpRequestMessage RequestFunc() // Deserialize Actor Response Message Body // Deserialize to ActorInvokeException when there is response header otherwise normal path - var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId); + var responseBodySerializer = serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); // actorResponseMessageHeader is not null, it means there is remote exception if (actorResponseMessageHeader != null) diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index a641440cf..c78126ccd 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -106,8 +106,8 @@ internal async Task> DispatchWithRemotingAsync(ActorId act var interfaceId = actorMessageHeader.InterfaceId; // Get the deserialized Body. - var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId); - + var msgBodySerializer = this.serializersManager.GetRequestMessageBodySerializer(actorMessageHeader.InterfaceId, actorMethodContext.MethodName); + IActorRequestMessageBody actorMessageBody; using (var stream = new MemoryStream()) { @@ -130,7 +130,7 @@ async Task> RequestFunc(Actor actor, CancellationToken ct) this.messageBodyFactory, ct); - return this.CreateResponseMessage(responseMsgBody, interfaceId); + return this.CreateResponseMessage(responseMsgBody, interfaceId, actorMethodContext.MethodName); } return await this.DispatchInternalAsync(actorId, actorMethodContext, RequestFunc, cancellationToken); @@ -386,12 +386,12 @@ private async Task DispatchInternalAsync(ActorId actorId, ActorMethodConte return retval; } - private Tuple CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId) + private Tuple CreateResponseMessage(IActorResponseMessageBody msgBody, int interfaceId, string methodName) { var responseMsgBodyBytes = Array.Empty(); if (msgBody != null) { - var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId); + var responseSerializer = this.serializersManager.GetResponseMessageBodySerializer(interfaceId, methodName); responseMsgBodyBytes = responseSerializer.Serialize(msgBody); } diff --git a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs index 28190a0d7..46455c28d 100644 --- a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs +++ b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,6 +11,7 @@ namespace Dapr.E2E.Test.Actors public interface ISerializationActor : IActor, IPingActor { Task SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default); + Task AnotherMethod(DateTime payload); } public record SerializationPayload(string Message) diff --git a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs index e8da59826..09e650fae 100644 --- a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs @@ -1,4 +1,5 @@  +using System; using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Runtime; @@ -22,5 +23,9 @@ public Task SendAsync(string name, { return Task.FromResult(payload); } + + public Task AnotherMethod(DateTime payload){ + return Task.FromResult(payload); + } } } diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs index c393f2ef1..5a20fff3d 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs @@ -84,5 +84,32 @@ public async Task ActorCanSupportCustomSerializer() Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value)); } } + + /// + /// This was actually a problem that is why the test exists. + /// It just checks, if the interface of the actor has more than one method defined, + /// that if can call it and serialize the payload correctly. + /// + /// + /// More than one methods means here, that in the exact interface must be two methods defined. + /// That excludes hirachies. + /// So wouldn't count here, because it's not directly defined in + /// . (it's defined in the base of it.) + /// That why was created, + /// so there are now more then one method. + /// + [Fact] + public async Task ActorCanSupportCustomSerializerAndCallMoreThenOneDefinedMethod() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); + + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); + + var payload = DateTime.MinValue; + var result = await proxy.AnotherMethod(payload); + + Assert.Equal(payload, result); + } } } diff --git a/test/Dapr.E2E.Test/DaprCommand.cs b/test/Dapr.E2E.Test/DaprCommand.cs index a692ec638..21e31365d 100644 --- a/test/Dapr.E2E.Test/DaprCommand.cs +++ b/test/Dapr.E2E.Test/DaprCommand.cs @@ -16,6 +16,7 @@ namespace Dapr.E2E.Test using System; using System.Collections.Generic; using System.Diagnostics; + using System.Drawing; using System.Linq; using System.Threading; using Xunit.Abstractions; @@ -23,6 +24,7 @@ namespace Dapr.E2E.Test public class DaprCommand { private readonly ITestOutputHelper output; + private readonly CircularBuffer logBuffer = new CircularBuffer(1000); public DaprCommand(ITestOutputHelper output) { @@ -66,7 +68,12 @@ public void Run() var done = outputReceived.WaitOne(this.Timeout); if (!done) { - throw new Exception($"Command: \"{this.Command}\" timed out while waiting for output: \"{this.OutputToMatch}\""); + var ex = new Exception($"Command: \"{this.Command}\" timed out while waiting for output: \"{this.OutputToMatch}\"{System.Environment.NewLine}" + + "This could also mean the E2E app had a startup error. For more details see the Data property of this exception."); + // we add here the log buffer of the last 1000 lines, of the application log + // to make it easier to debug failing tests + ex.Data.Add("log", this.logBuffer.ToArray()); + throw ex; } } @@ -79,8 +86,7 @@ private void CheckOutput(object sendingProcess, DataReceivedEventArgs e) try { - // see: https://github.com/xunit/xunit/issues/2146 - this.output.WriteLine(e.Data.TrimEnd(Environment.NewLine.ToCharArray())); + WriteLine(e.Data); } catch (InvalidOperationException) { @@ -101,12 +107,81 @@ private void OnErrorOutput(object sender, DataReceivedEventArgs e) try { - // see: https://github.com/xunit/xunit/issues/2146 - this.output.WriteLine(e.Data.TrimEnd(Environment.NewLine.ToCharArray())); + WriteLine(e.Data); } catch (InvalidOperationException) { } } + + private void WriteLine(string message) + { + // see: https://github.com/xunit/xunit/issues/2146 + var formattedMessage = message.TrimEnd(Environment.NewLine.ToCharArray()); + this.output.WriteLine(formattedMessage); + this.logBuffer.Add(formattedMessage); + } + } + + /// + /// A circular buffer that can be used to store a fixed number of items. + /// When the buffer is full, the oldest item is overwritten. + /// The buffer can be read in the same order as the items were added. + /// More information can be found here. + /// + /// + /// The buffer gets initialized by the call to the constructor and will allocate, + /// the memory for the buffer. The buffer is not resizable. + /// That means be carefull with , because it can cause an . + /// + /// The type of what the cicular buffer is off. + internal class CircularBuffer{ + private readonly int size; + private readonly T[] buffer; + private int readPosition = 0; + private int writePosition = 0; + /// + /// Initialize the buffer with the buffer size of . + /// + /// + /// The size the buffer will have + /// + public CircularBuffer(int size) + { + this.size = size; + buffer = new T[size]; + } + /// + /// Adds an item and move the write position to the next value + /// + /// The item that should be written. + public void Add(T item) + { + buffer[writePosition] = item; + writePosition = (writePosition + 1) % size; + } + /// + /// Reads on value and move the position to the next value + /// + /// + public T Read(){ + var value = buffer[readPosition]; + readPosition = (readPosition + 1) % size; + return value; + } + /// + /// Read the full buffer. + /// While the buffer is read, the read position is moved to the next value + /// + /// + public T[] ToArray() + { + var result = new T[size]; + for (int i = 0; i < size; i++) + { + result[i] = Read(); + } + return result; + } } } From 2afde01d2068ed36a73628b41b26ad84dbe936b4 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 3 Dec 2024 22:50:42 -0700 Subject: [PATCH 45/69] Support .NET 9 (#1404) * Updated build and integration test scripts to include .NET 9 Signed-off-by: Whit Waldo * Removed unused matrix values Signed-off-by: Whit Waldo * Reverted some .NET 8 requirement Signed-off-by: Whit Waldo * Updated setup-dotnet to use latest action version + updated script to prefer a GA release, but use RC if available. Signed-off-by: Whit Waldo * Removed unnecessary secondary build step Signed-off-by: Whit Waldo * Updating TFM moniker Signed-off-by: Whit Waldo * Added test to install VStest Signed-off-by: Whit Waldo * Rolling back use of tool as it doesn't independently exist outside of the SDK Signed-off-by: Whit Waldo * Added .NET 9 to build targets Signed-off-by: Whit Waldo * Added .NET 9 to target frameworks across solution Signed-off-by: Whit Waldo * I understand the reason for the required install step now - adding it back with a .NET 9 install step Signed-off-by: Whit Waldo * Placing install steps before build Signed-off-by: Whit Waldo * Updating global.json Signed-off-by: Whit Waldo * Disabled analyzer errors in unit tests Signed-off-by: Whit Waldo * Added .NET 9 to test Signed-off-by: Whit Waldo * Changed from #pragma error to #pragma warning Signed-off-by: Whit Waldo * Fixed unit tests to resolve analyzer warning Signed-off-by: Whit Waldo * Updated integration test to always include .NET 8 and .NET 9 installs Signed-off-by: Whit Waldo * Falling back to add separate .NET 9 support Signed-off-by: Whit Waldo * Updated referenced projects to target appropriate frameworks Signed-off-by: Whit Waldo * Added all target frameworks back to Dapr.Commono Signed-off-by: Whit Waldo * Added warnings to fix nullability analyzer warnings when targeting .NET 6 Signed-off-by: Whit Waldo * Updated build step to use .NET 9 instead Signed-off-by: Whit Waldo * Fixed cloud event middleware tests - the ApplicationBuilder requires a non-null ServiceProvider per https://learn.microsoft.com/en-us/dotnet/core/compatibility/extensions/8.0/activatorutilities-createinstance-null-provider Signed-off-by: Whit Waldo * Including target for .NET 6, 7, 8 and 9 Signed-off-by: Whit Waldo * Trialing fix to E2E integration test - excluding use of AppWebApplicationFactory in favor of direct use of HttpClient Signed-off-by: Whit Waldo * Reverting as it breaks the other .NET versions Signed-off-by: Whit Waldo * Potentially fixed unit tests in .NET 9 Signed-off-by: Whit Waldo * Removed extra line from build definition Signed-off-by: Whit Waldo * Updated documentation to reflect .NET 9 and a note highlighting that .NET 6 and .NET 7 will be deprecated in v1.16 Signed-off-by: Whit Waldo * Removed unintentionally added file to commit Signed-off-by: Whit Waldo * Added .NET 9 to E2E test setup Signed-off-by: Whit Waldo * Fixed typo Signed-off-by: Whit Waldo * Removed RC version from .NET 9 build Signed-off-by: Whit Waldo * Apparently the solution file got a minor change Signed-off-by: Whit Waldo * Removed unnecessary null checks Signed-off-by: Whit Waldo * Whoops - didn't mean to commit that project to the solution Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .github/workflows/itests.yml | 23 +- .github/workflows/sdk_build.yml | 31 ++- all.sln | 4 +- daprdocs/content/en/dotnet-sdk-docs/_index.md | 10 +- .../dotnet-actors/dotnet-actors-howto.md | 10 +- .../dotnet-jobs/dotnet-jobs-howto.md | 11 +- .../dotnet-workflow/dotnet-workflow-howto.md | 10 +- global.json | 4 +- src/Dapr.Common/Dapr.Common.csproj | 2 +- src/Dapr.Jobs/Dapr.Jobs.csproj | 1 - src/Dapr.Jobs/Models/DaprJobSchedule.cs | 4 +- src/Dapr.Protos/Dapr.Protos.csproj | 1 + src/Dapr.Workflow/Dapr.Workflow.csproj | 1 + src/Directory.Build.props | 2 +- .../CSharpSourceGeneratorVerifier.cs | 2 + .../CloudEventsIntegrationTest.cs | 6 +- .../Dapr.AspNetCore.IntegrationTest.csproj | 26 ++- .../CloudEventsMiddlewareTest.cs | 199 ++++++++++++------ test/Dapr.Client.Test/DaprClientTest.cs | 2 +- test/Dapr.E2E.Test/DaprTestApp.cs | 1 + test/Directory.Build.props | 2 +- 21 files changed, 255 insertions(+), 97 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index d06c12cd5..4dcdfb951 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0'] + dotnet-version: ['6.0', '7.0', '8.0', '9.0'] include: - dotnet-version: '6.0' display-name: '.NET 6.0' @@ -37,6 +37,11 @@ jobs: framework: 'net8' prefix: 'net8' install-version: '8.0.x' + - dotnet-version: '9.0' + display-name: '.NET 9.0' + framework: 'net9' + prefix: 'net9' + install-version: '9.0.x' env: NUPKG_OUTDIR: bin/Release/nugets GOVER: 1.20.3 @@ -103,14 +108,22 @@ jobs: - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup ${{ matrix.display-name }} - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 8.0 # net8 is always required. - uses: actions/setup-dotnet@v1 + dotnet-quality: 'ga' # Prefer a GA release, but use the RC if not available + - name: Setup .NET 8 (required) + uses: actions/setup-dotnet@v3 if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' + dotnet-quality: 'ga' + - name: Setup .NET 9 (required) + uses: actions/setup-dotnet@v3 + if: ${{ matrix.install-version != '9.0.x' }} + with: + dotnet-version: '9.0.x' + dotnet-quality: 'ga' - 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 diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 5e6fd3532..b6e263530 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -24,9 +24,10 @@ jobs: - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x + dotnet-quality: 'ga' - name: Build run: dotnet build --configuration release - name: Generate Packages @@ -43,39 +44,49 @@ jobs: strategy: fail-fast: false matrix: - dotnet-version: ['6.0', '7.0', '8.0'] + dotnet-version: ['6.0', '7.0', '8.0', '9.0'] include: - dotnet-version: '6.0' - install-3: false display-name: '.NET 6.0' framework: 'net6' prefix: 'net6' install-version: '6.0.x' - dotnet-version: '7.0' - install-3: false display-name: '.NET 7.0' framework: 'net7' prefix: 'net7' install-version: '7.0.x' - dotnet-version: '8.0' - install-3: false display-name: '.NET 8.0' framework: 'net8' prefix: 'net8' install-version: '8.0.x' + - dotnet-version: '9.0' + display-name: '.NET 9.0' + framework: 'net9' + prefix: 'net9' + install-version: '9.0.x' steps: - uses: actions/checkout@v1 - name: Parse release version run: python ./.github/scripts/get_release_version.py - name: Setup ${{ matrix.display-name }} - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 8.0 # net8 is always required. - uses: actions/setup-dotnet@v1 + dotnet-quality: 'ga' # Prefer a GA release, but use the RC if not available + - name: Setup .NET 8 (required) + uses: actions/setup-dotnet@v3 if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' + dotnet-quality: 'ga' + - name: Setup .NET 9 (required) + uses: actions/setup-dotnet@v3 + if: ${{ matrix.install-version != '9.0.x' }} + with: + dotnet-version: '9.0.x' + dotnet-quality: 'ga' - 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 diff --git a/all.sln b/all.sln index 8a6eb2fff..a3de1f6b8 100644 --- a/all.sln +++ b/all.sln @@ -1,5 +1,5 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# 17 +# Visual Studio Version 17 VisualStudioVersion = 17.3.32929.385 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors", "src\Dapr.Actors\Dapr.Actors.csproj", "{C2DB4B64-B7C3-4FED-8753-C040F677C69A}" @@ -364,7 +364,7 @@ Global {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Debug|Any CPU.Build.0 = Debug|Any CPU {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.ActiveCfg = Release|Any CPU - {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CP + {290D1278-F613-4DF3-9DF5-F37E38CDC363}.Release|Any CPU.Build.0 = Release|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8BB6A85-A7EA-40C0-893D-F36F317829B3}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 60a4a1a61..82d16016d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,7 +18,15 @@ Dapr offers a variety of packages to help with the development of .NET applicati - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [.NET 6](https://dotnet.microsoft.com/download) or [.NET 8+](https://dotnet.microsoft.com/download) installed +- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## 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 eaa13625d..aba62bf07 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,15 @@ This project contains the implementation of the actor client which calls MyActor - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed. - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}). -- [.NET 6+](https://dotnet.microsoft.com/download) 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), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Step 0: Prepare diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md index c8bc66175..8d98d1ca5 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -16,10 +16,17 @@ In the .NET example project: - The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample/Program.cs) file comprises the entirety of this demonstration. ## 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 Jobs .NET SDK](https://github.com/dapr/dotnet-sdk) +- [.NET 6](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed + +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Set up the environment Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md index f6d18bc58..8a6e6665e 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md @@ -18,11 +18,17 @@ In the .NET example project: ## 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://github.com/dapr/dotnet-sdk/) +- [.NET 7](https://dotnet.microsoft.com/download), [.NET 8](https://dotnet.microsoft.com/download) or [.NET 9](https://dotnet.microsoft.com/download) installed +{{% alert title="Note" color="primary" %}} + +Note that while .NET 6 is generally supported as the minimum .NET requirement across the Dapr .NET SDK packages +and .NET 7 is the minimally supported version of .NET by Dapr.Workflows in Dapr v1.15, only .NET 8 and .NET 9 will +continue to be supported by Dapr in v1.16 and later. + +{{% /alert %}} ## Set up the environment diff --git a/global.json b/global.json index fe53f92ae..139cca3e3 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "_comment": "This policy allows the 8.0.100 SDK or patches in that family.", "sdk": { - "version": "8.0.100", - "rollForward": "minor" + "version": "9.0.100", + "rollForward": "latestFeature" } } \ No newline at end of file diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index 31af3952c..d1e106b6d 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -1,7 +1,7 @@  - net6;net7;net8 + net6;net7;net8;net9 enable enable diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj index 74c9bec23..9f209b0bd 100644 --- a/src/Dapr.Jobs/Dapr.Jobs.csproj +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -1,7 +1,6 @@  - net6;net8 enable enable Dapr.Jobs diff --git a/src/Dapr.Jobs/Models/DaprJobSchedule.cs b/src/Dapr.Jobs/Models/DaprJobSchedule.cs index c1b592e12..e00c77f49 100644 --- a/src/Dapr.Jobs/Models/DaprJobSchedule.cs +++ b/src/Dapr.Jobs/Models/DaprJobSchedule.cs @@ -67,7 +67,6 @@ public static DaprJobSchedule FromCronExpression(CronExpressionBuilder builder) /// public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) { - ArgumentNullException.ThrowIfNull(scheduledTime, nameof(scheduledTime)); return new DaprJobSchedule(scheduledTime.ToString("O")); } @@ -77,7 +76,9 @@ public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) /// The systemd Cron-like expression indicating when the job should be triggered. public static DaprJobSchedule FromExpression(string expression) { +#if NET6_0 ArgumentNullException.ThrowIfNull(expression, nameof(expression)); +#endif return new DaprJobSchedule(expression); } @@ -87,7 +88,6 @@ public static DaprJobSchedule FromExpression(string expression) /// The duration interval. public static DaprJobSchedule FromDuration(TimeSpan duration) { - ArgumentNullException.ThrowIfNull(duration, nameof(duration)); return new DaprJobSchedule(duration.ToDurationString()); } diff --git a/src/Dapr.Protos/Dapr.Protos.csproj b/src/Dapr.Protos/Dapr.Protos.csproj index 8a8804b22..5331f229c 100644 --- a/src/Dapr.Protos/Dapr.Protos.csproj +++ b/src/Dapr.Protos/Dapr.Protos.csproj @@ -1,6 +1,7 @@  + net6;net7;net8;net9 enable enable This package contains the reference protos used by develop services using Dapr. diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 360d121ef..f24d41e40 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -3,6 +3,7 @@ + net6;net7;net8;net9 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 35f0fbf7c..a74833a37 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - net6;net8 + net6;net8;net9 $(RepoRoot)bin\$(Configuration)\prod\$(MSBuildProjectName)\ $(OutputPath)$(MSBuildProjectName).xml diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index c64fd3427..ef1d7b9a0 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -34,6 +34,8 @@ public Test() 7; #elif NET8_0 8; +#elif NET9_0 + 9; #endif // diff --git a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs index dd940a75d..9b0b5d3a3 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/CloudEventsIntegrationTest.cs @@ -146,7 +146,7 @@ public async Task CanSendBinaryCloudEvent_WithContentType() using (var factory = new AppWebApplicationFactory()) { var httpClient = factory.CreateClient(new Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactoryClientOptions { HandleCookies = false }); - + var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost/register-user") { Content = new StringContent( @@ -158,10 +158,10 @@ public async Task CanSendBinaryCloudEvent_WithContentType() Encoding.UTF8) }; request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - + var response = await httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); - + var userInfo = await JsonSerializer.DeserializeAsync(await response.Content.ReadAsStreamAsync(), this.options); userInfo.Name.Should().Be("jimmy"); } diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index d51dc70e8..d7dd6d52a 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -7,14 +7,36 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + - + + + + + + + + + + + + diff --git a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs index 9c1f1e005..2f9fab936 100644 --- a/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs +++ b/test/Dapr.AspNetCore.Test/CloudEventsMiddlewareTest.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.DependencyInjection; + namespace Dapr.AspNetCore.Test { using System.IO; @@ -33,7 +35,10 @@ public class CloudEventsMiddlewareTest [InlineData("application/cloudevents-batch+json")] // we don't support batch public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) { - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -46,9 +51,10 @@ public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = contentType; - context.Request.Body = MakeBody("Hello, world!"); + var context = new DefaultHttpContext + { + Request = { ContentType = contentType, Body = MakeBody("Hello, world!") } + }; await pipeline.Invoke(context); } @@ -62,7 +68,10 @@ public async Task InvokeAsync_IgnoresOtherContentTypes(string contentType) public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -75,11 +84,17 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch 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); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -93,7 +108,10 @@ public async Task InvokeAsync_ReplacesBodyJson(string dataContentType, string ch public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -106,11 +124,17 @@ public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType 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); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"Data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"DataContentType\": \"{dataContentType}\", \"Data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -124,7 +148,10 @@ public async Task InvokeAsync_ReplacesPascalCasedBodyJson(string dataContentType public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true @@ -143,11 +170,17 @@ public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContent 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); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -161,7 +194,10 @@ public async Task InvokeAsync_ForwardsJsonPropertiesAsHeaders(string dataContent public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true, @@ -181,11 +217,17 @@ public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dat 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); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -199,7 +241,10 @@ public async Task InvokeAsync_ForwardsIncludedJsonPropertiesAsHeaders(string dat public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(string dataContentType, string charSet) { var encoding = charSet == null ? null : Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions { ForwardCloudEventPropertiesAsHeaders = true, @@ -219,11 +264,17 @@ public async Task InvokeAsync_DoesNotForwardExcludedJsonPropertiesAsHeaders(stri 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); + var context = new DefaultHttpContext { Request = + { + ContentType = + charSet == null + ? "application/cloudevents+json" + : $"application/cloudevents+json;charset={charSet}", + Body = dataContentType == null ? + MakeBody("{ \"type\": \"Test.Type\", \"subject\": \"Test.Subject\", \"data\": { \"name\":\"jimmy\" } }", encoding) : + MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"type\":\"Test.Type\", \"subject\": \"Test.Subject\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -234,10 +285,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() // Our logic is based on the content-type, not the content. // Since this is for text-plain content, we're going to encode it as a JSON string // and store it in the data attribute - the middleware should JSON-decode it. - var input = "{ \"message\": \"hello, world\"}"; + const string input = "{ \"message\": \"hello, world\"}"; var expected = input; - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -251,9 +305,12 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; - context.Request.Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}"); + var context = new DefaultHttpContext { Request = + { + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; await pipeline.Invoke(context); } @@ -262,10 +319,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData() public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() { // Our logic is based on the content-type, not the content. This test tests the old bad behavior. - var input = "{ \"message\": \"hello, world\"}"; + const string input = "{ \"message\": \"hello, world\"}"; var expected = JsonSerializer.Serialize(input); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(new CloudEventsMiddlewareOptions() { SuppressJsonDecodingOfTextPayloads = true, }); // Do verification in the scope of the middleware @@ -279,9 +339,12 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; - context.Request.Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}"); + var context = new DefaultHttpContext { Request = + { + ContentType = "application/cloudevents+json", + Body = MakeBody($"{{ \"datacontenttype\": \"text/plain\", \"data\": {JsonSerializer.Serialize(input)} }}") + } + }; await pipeline.Invoke(context); } @@ -291,10 +354,13 @@ public async Task InvokeAsync_ReplacesBodyNonJsonData_ExceptWhenSuppressed() [Fact] public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() { - var dataContentType = "application/person+json;charset=UTF-16"; - var charSet = "UTF-16"; + const string dataContentType = "application/person+json;charset=UTF-16"; + const string charSet = "UTF-16"; var encoding = Encoding.GetEncoding(charSet); - var app = new ApplicationBuilder(null); + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); // Do verification in the scope of the middleware @@ -307,10 +373,11 @@ public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = $"application/cloudevents+json;charset={charSet}"; - context.Request.Body = - MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding); + var context = new DefaultHttpContext { Request = + { + ContentType = $"application/cloudevents+json;charset={charSet}", Body = MakeBody($"{{ \"datacontenttype\": \"{dataContentType}\", \"data\": {{ \"name\":\"jimmy\" }} }}", encoding) + } + }; await pipeline.Invoke(context); } @@ -318,8 +385,11 @@ public async Task InvokeAsync_ReplacesBodyJson_NormalizesPayloadCharset() [Fact] public async Task InvokeAsync_ReadsBinaryData() { - var dataContentType = "application/octet-stream"; - var app = new ApplicationBuilder(null); + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); var data = new byte[] { 1, 2, 3 }; @@ -328,15 +398,18 @@ public async Task InvokeAsync_ReadsBinaryData() { httpContext.Request.ContentType.Should().Be(dataContentType); var bytes = new byte[httpContext.Request.Body.Length]; +#if NET9_0 + httpContext.Request.Body.ReadExactly(bytes, 0, bytes.Length); +#else httpContext.Request.Body.Read(bytes, 0, bytes.Length); +#endif bytes.Should().Equal(data); return Task.CompletedTask; }); var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; var base64Str = System.Convert.ToBase64String(data); context.Request.Body = @@ -348,10 +421,13 @@ public async Task InvokeAsync_ReadsBinaryData() [Fact] public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() { - var dataContentType = "application/octet-stream"; - var app = new ApplicationBuilder(null); + const string dataContentType = "application/octet-stream"; + var serviceCollection = new ServiceCollection(); + var provider = serviceCollection.BuildServiceProvider(); + + var app = new ApplicationBuilder(provider); app.UseCloudEvents(); - var data = "{\"id\": \"1\"}"; + const string data = "{\"id\": \"1\"}"; // Do verification in the scope of the middleware app.Run(httpContext => @@ -364,8 +440,7 @@ public async Task InvokeAsync_DataAndData64Set_ReturnsBadRequest() var pipeline = app.Build(); - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/cloudevents+json"; + var context = new DefaultHttpContext { Request = { ContentType = "application/cloudevents+json" } }; var bytes = Encoding.UTF8.GetBytes(data); var base64Str = System.Convert.ToBase64String(bytes); context.Request.Body = @@ -391,7 +466,11 @@ private static string ReadBody(Stream stream, Encoding encoding = null) encoding ??= Encoding.UTF8; var bytes = new byte[stream.Length]; +#if NET9_0 + stream.ReadExactly(bytes, 0, bytes.Length); +#else stream.Read(bytes, 0, bytes.Length); +#endif var str = encoding.GetString(bytes); return str; } diff --git a/test/Dapr.Client.Test/DaprClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.cs index 01d22edcf..e280728c2 100644 --- a/test/Dapr.Client.Test/DaprClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.cs @@ -45,7 +45,7 @@ public void CreateInvokeHttpClient_WithoutAppId() var client = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); Assert.Null(client.BaseAddress); } - + [Fact] public void CreateInvokeHttpClient_InvalidDaprEndpoint_InvalidFormat() { diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 152aeee98..2330785d8 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -139,6 +139,7 @@ private static string GetTargetFrameworkName() ".NETCoreApp,Version=v6.0" => "net6", ".NETCoreApp,Version=v7.0" => "net7", ".NETCoreApp,Version=v8.0" => "net8", + ".NETCoreApp,Version=v9.0" => "net9", _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") }; } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 50b029a12..e3a49b72f 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,7 +2,7 @@ - net6;net7;net8 + net6;net7;net8;net9 $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ From b343ecb2e531e42ba2648f6c189df10f732781e3 Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:12:02 -0500 Subject: [PATCH 46/69] update .net workflow docs to stable (#1418) Signed-off-by: Hannah Hunter Signed-off-by: Siri Varma Vegiraju --- .../dotnet-workflow/dotnet-workflow-howto.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md index 8a6e6665e..9be910234 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md @@ -89,7 +89,7 @@ Run the following command to start a workflow. {{% codetab %}} ```bash -curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ +curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -99,7 +99,7 @@ curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingW {{% codetab %}} ```powershell -curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` +curl -i -X POST http://localhost:3500/v1.0/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` -H "Content-Type: application/json" ` -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -117,7 +117,7 @@ If successful, you should see a response like the following: Send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/12345678 +curl -i -X GET http://localhost:3500/v1.0/workflows/dapr/12345678 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed when you issue the HTTP request, you'll see the following JSON response (formatted for readability) with workflow status as `RUNNING`: From 4ed55b30c3d32cc3adb654c1dc607f93d96f8c39 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:36:45 +0100 Subject: [PATCH 47/69] FIX: Actor source generator generates invalid code for generic interfaces (#1419) * Handled generic actor interface Signed-off-by: Manuel Menegazzo * Added more actor examples Signed-off-by: Manuel Menegazzo * Updated actor namespace in example project Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo Co-authored-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../ActorClient/IGenericClientActor.cs | 27 +++ .../ActorClientGenerator.cs | 23 ++- .../ActorClientGeneratorTests.cs | 172 ++++++++++++++++++ 3 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 examples/GeneratedActor/ActorClient/IGenericClientActor.cs diff --git a/examples/GeneratedActor/ActorClient/IGenericClientActor.cs b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs new file mode 100644 index 000000000..166f4a9ef --- /dev/null +++ b/examples/GeneratedActor/ActorClient/IGenericClientActor.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// 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 +{ + [GenerateActorClient] + internal interface IGenericClientActor + { + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(TGenericType2 state, CancellationToken cancellationToken = default); + } +} diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index 001604d53..0f064e801 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -161,12 +161,23 @@ private static void GenerateActorClientCode(SourceProductionContext context, Act .Append(SyntaxKind.SealedKeyword) .Select(sk => SyntaxFactory.Token(sk)); - var actorClientClassDeclaration = SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) - .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) - .WithMembers(SyntaxFactory.List(actorMembers)) - .WithBaseList(SyntaxFactory.BaseList( - SyntaxFactory.Token(SyntaxKind.ColonToken), - SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); + var actorClientClassTypeParameters = descriptor.InterfaceType.TypeParameters + .Select(x => SyntaxFactory.TypeParameter(x.ToString())); + + var actorClientClassDeclaration = (actorClientClassTypeParameters.Count() == 0) + ? SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))) + : SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithTypeParameterList(SyntaxFactory.TypeParameterList(SyntaxFactory.SeparatedList(actorClientClassTypeParameters))) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(descriptor.NamespaceName)) .WithMembers(SyntaxFactory.List(new[] { actorClientClassDeclaration })) diff --git a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs index 4c0ef194e..3515bc8b0 100644 --- a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -168,6 +168,92 @@ public System.Threading.Tasks.Task TestMethod() await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } + [Fact] + public async Task TestSingleGenericInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(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 TestMultipleGenericsInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(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() { @@ -211,6 +297,92 @@ public System.Threading.Tasks.Task TestMethod() await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } + [Fact] + public async Task TestSingleGenericRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(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 TestMultipleGenericsRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +}"; + + var generatedSource = @"// +#nullable enable +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(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() { From d21a686789ac727a5ed12504a5afc953a33f481d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 10 Dec 2024 09:06:44 -0600 Subject: [PATCH 48/69] Add .NET client for LLM Conversations support (#1382) * Updated prototype Signed-off-by: Whit Waldo * Added Dapr.AI project and unit test project to contain the conversational building block (and potentially future other projects) Signed-off-by: Whit Waldo * Changed default values Signed-off-by: Whit Waldo * Removed unnecessary method Signed-off-by: Whit Waldo * Added a few unit tests Signed-off-by: Whit Waldo * Added example project Signed-off-by: Whit Waldo * Added missing copyright headers Signed-off-by: Whit Waldo * Changed type name -> DaprLlmInput to DaprConversationInput Signed-off-by: Whit Waldo * Returning read only list Signed-off-by: Whit Waldo * Update to use IReadOnlyDictionary Signed-off-by: Whit Waldo * Added method to abstract class Signed-off-by: Whit Waldo * Striving for consistency in how properties are specified on the record Signed-off-by: Whit Waldo * Refactored enum extensions out to Dapr.Common since it will be used in AI project Signed-off-by: Whit Waldo * Added JSON converter for System.Text.Json to handle enum serialization based on the enum member attributes Signed-off-by: Whit Waldo * Added unit tests to prove out generic enum JSON converter using EnumMember attributes Signed-off-by: Whit Waldo * Added JSON converter to new enum for Dapr Conversation role Signed-off-by: Whit Waldo * Set up role to map to the string used in grpc call to sidecar Signed-off-by: Whit Waldo * No need for the JSON converter after all Signed-off-by: Whit Waldo * Added missing package version to fix build error Signed-off-by: Whit Waldo * Removed duplicate using statement breaking build Signed-off-by: Whit Waldo * Fixed missing [Fact] annotation Signed-off-by: Whit Waldo * Updated proto types to reflect type name changes in https://github.com/dapr/dapr/pull/8250 Signed-off-by: Whit Waldo * Added support for service lifetime Signed-off-by: Whit Waldo * Building out documentation for Dapr AI Signed-off-by: Whit Waldo * Simplified registration Signed-off-by: Whit Waldo * Tweaked package version Signed-off-by: Whit Waldo * Using IConfiguration to source DaprClient values if provided in service provider Signed-off-by: Whit Waldo * Removed Models.* directories, flattened into Conversation namespace Signed-off-by: Whit Waldo * Swapped out to use IReadOnlyDictionary Signed-off-by: Whit Waldo * Added suggested optimization Signed-off-by: Whit Waldo * Fixed bad using statement Signed-off-by: Whit Waldo * Updates to use uniform method for standing up new Dapr clients Signed-off-by: Whit Waldo * Removed duplicate project reference Signed-off-by: Whit Waldo * Fixed build error Signed-off-by: Whit Waldo * Fixing build errors Signed-off-by: Whit Waldo * Fixed bad references Signed-off-by: Whit Waldo * Fixed several build errors Signed-off-by: Whit Waldo * Fixing more build errors Signed-off-by: Whit Waldo * Updated to fix several build errors Signed-off-by: Whit Waldo * Fixed bad refernce Signed-off-by: Whit Waldo * Fixing more build errors Signed-off-by: Whit Waldo * Role is required when submitting conversation input Signed-off-by: Whit Waldo * Removed impossible path since the role cannot be nullable Signed-off-by: Whit Waldo * Removed impossible path from logic now that role cannot be null Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- Directory.Packages.props | 100 +- all.sln | 24 + daprdocs/content/en/dotnet-sdk-docs/_index.md | 7 + .../en/dotnet-sdk-docs/dotnet-ai/_index.md | 83 + .../dotnet-ai/dotnet-ai-usage.md | 7 + .../ConversationalAI/ConversationalAI.csproj | 13 + examples/AI/ConversationalAI/Program.cs | 23 + src/Dapr.AI/AssemblyInfo.cs | 4 + .../Conversation/ConversationOptions.cs | 40 + .../Conversation/DaprConversationClient.cs | 116 + .../DaprConversationClientBuilder.cs | 46 + .../Conversation/DaprConversationInput.cs | 22 + .../Conversation/DaprConversationResponse.cs | 21 + .../Conversation/DaprConversationResult.cs | 28 + .../Conversation/DaprConversationRole.cs | 42 + .../Extensions/DaprAiConversationBuilder.cs | 35 + .../DaprAiConversationBuilderExtensions.cs | 64 + .../Extensions/IDaprAiConversationBuilder.cs | 23 + src/Dapr.AI/Dapr.AI.csproj | 26 + src/Dapr.AI/DaprAIClient.cs | 34 + .../Extensions/IDaprAiServiceBuilder.cs | 27 + src/Dapr.Client/DaprClientGrpc.cs | 3709 +++++++++-------- src/Dapr.Client/Extensions/EnumExtensions.cs | 41 - src/Dapr.Common/AssemblyInfo.cs | 4 + src/Dapr.Common/DaprClientUtilities.cs | 65 + src/Dapr.Common/DaprGenericClientBuilder.cs | 51 +- src/Dapr.Common/Extensions/EnumExtensions.cs | 15 +- .../GenericEnumJsonConverter.cs | 70 + src/Dapr.Jobs/DaprJobsClientBuilder.cs | 16 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 82 +- .../DaprJobsServiceCollectionExtensions.cs | 44 +- .../DaprPublishSubscribeClientBuilder.cs | 5 +- .../DaprPublishSubscribeGrpcClient.cs | 30 +- ...ishSubscribeServiceCollectionExtensions.cs | 6 +- .../Protos/dapr/proto/common/v1/common.proto | 2 +- .../dapr/proto/runtime/v1/appcallback.proto | 2 +- .../Protos/dapr/proto/runtime/v1/dapr.proto | 57 +- .../DaprConversationClientBuilderTest.cs | 33 + ...DaprAiConversationBuilderExtensionsTest.cs | 75 + test/Dapr.AI.Test/Dapr.AI.Test.csproj | 28 + .../Extensions/EnumExtensionTest.cs | 38 - .../Extensions/EnumExtensionsTest.cs | 6 +- .../GenericEnumJsonConverterTest.cs | 52 + ...aprJobsServiceCollectionExtensionsTests.cs | 61 +- 44 files changed, 3165 insertions(+), 2112 deletions(-) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md create mode 100644 examples/AI/ConversationalAI/ConversationalAI.csproj create mode 100644 examples/AI/ConversationalAI/Program.cs create mode 100644 src/Dapr.AI/AssemblyInfo.cs create mode 100644 src/Dapr.AI/Conversation/ConversationOptions.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationClient.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationInput.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationResponse.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationResult.cs create mode 100644 src/Dapr.AI/Conversation/DaprConversationRole.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs create mode 100644 src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs create mode 100644 src/Dapr.AI/Dapr.AI.csproj create mode 100644 src/Dapr.AI/DaprAIClient.cs create mode 100644 src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs delete mode 100644 src/Dapr.Client/Extensions/EnumExtensions.cs create mode 100644 src/Dapr.Common/DaprClientUtilities.cs create mode 100644 src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs create mode 100644 test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs create mode 100644 test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs create mode 100644 test/Dapr.AI.Test/Dapr.AI.Test.csproj delete mode 100644 test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs create mode 100644 test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index a98e9db58..efb48fcc4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,52 @@ - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/all.sln b/all.sln index a3de1f6b8..405170b78 100644 --- a/all.sln +++ b/all.sln @@ -119,6 +119,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common", "src\Dapr.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{CDB47863-BEBD-4841-A807-46D868962521}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI", "src\Dapr.AI\Dapr.AI.csproj", "{273F2527-1658-4CCF-8DC6-600E921188C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.AI.Test", "test\Dapr.AI.Test\Dapr.AI.Test.csproj", "{2F3700EF-1CDA-4C15-AC88-360230000ECD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AI", "AI", "{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConversationalAI", "examples\AI\ConversationalAI\ConversationalAI.csproj", "{11011FF8-77EA-4B25-96C0-29D4D486EF1C}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowExternalInteraction", "examples\Workflow\WorkflowExternalInteraction\WorkflowExternalInteraction.csproj", "{43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkflowMonitor", "examples\Workflow\WorkflowMonitor\WorkflowMonitor.csproj", "{7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6}" @@ -329,6 +337,18 @@ Global {CDB47863-BEBD-4841-A807-46D868962521}.Debug|Any CPU.Build.0 = Debug|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.ActiveCfg = Release|Any CPU {CDB47863-BEBD-4841-A807-46D868962521}.Release|Any CPU.Build.0 = Release|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {273F2527-1658-4CCF-8DC6-600E921188C5}.Release|Any CPU.Build.0 = Release|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F3700EF-1CDA-4C15-AC88-360230000ECD}.Release|Any CPU.Build.0 = Release|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11011FF8-77EA-4B25-96C0-29D4D486EF1C}.Release|Any CPU.Build.0 = Release|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -433,6 +453,10 @@ Global {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 82d16016d..ce80b3ea9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -84,6 +84,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria +
+
+
AI
+

Create and manage AI operations in .NET

+ +
+
## More information diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md new file mode 100644 index 000000000..4374a8598 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md @@ -0,0 +1,83 @@ +_index.md + +--- +type: docs +title: "Getting started with the Dapr AI .NET SDK client" +linkTitle: "AI" +weight: 10000 +description: How to get up and running with the Dapr AI .NET SDK +no_list: true +--- + +The Dapr AI client package allows you to interact with the AI capabilities provided by the Dapr sidecar. + +## Installation + +To get started with the Dapr AI .NET SDK client, install the following package from NuGet: +```sh +dotnet add package Dapr.AI +``` + +A `DaprConversationClient` holes access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar. + +### Dependency Injection + +The `AddDaprAiConversation()` method will register the Dapr client ASP.NET Core dependency injection and is the recommended approach +for using this package. This method accepts an optional options delegate for configuring the `DaprConversationClient` and a +`ServiceLifetime` argument, allowing you to specify a different lifetime for the registered services instead of the default `Singleton` +value. + +The following example assumes all default values are acceptable and is sufficient to register the `DaprConversationClient`: + +```csharp +services.AddDaprAiConversation(); +``` + +The optional configuration delegate is used to configure the `DaprConversationClient` by specifying options on the +`DaprConversationClientBuilder` as in the following example: +```csharp +services.AddSingleton(); +services.AddDaprAiConversation((serviceProvider, clientBuilder) => { + //Inject a service to source a value from + var optionsProvider = serviceProvider.GetRequiredService(); + var standardTimeout = optionsProvider.GetStandardTimeout(); + + //Configure the value on the client builder + clientBuilder.UseTimeout(standardTimeout); +}); +``` + +### Manual Instantiation +Rather than using dependency injection, a `DaprConversationClient` can also be built using the static client builder. + +For best performance, create a single long-lived instance of `DaprConversationClient` and provide access to that shared instance throughout +your application. `DaprConversationClient` instances are thread-safe and intended to be shared. + +Avoid creating a `DaprConversationClient` per-operation. + +A `DaprConversationClient` can be configured by invoking methods on the `DaprConversationClientBuilder` class before calling `.Build()` +to create the client. The settings for each `DaprConversationClient` are separate and cannot be changed after calling `.Build()`. + +```csharp +var daprConversationClient = new DaprConversationClientBuilder() + .UseJsonSerializerSettings( ... ) //Configure JSON serializer + .Build(); +``` + +See the .NET [documentation here]({{< ref dotnet-client >}}) for more information about the options available when configuring the Dapr client via the builder. + +## Try it out +Put the Dapr AI .NET SDK to the test. Walk through the samples to see Dapr in action: + +| SDK Samples | Description | +| ----------- | ----------- | +| [SDK samples](https://github.com/dapr/dotnet-sdk/tree/master/examples) | Clone the SDK repo to try out some examples and get started. | + +## Building Blocks + +This part of the .NET SDK allows you to interface with the Conversations API to send and receive messages from +large language models. + +### Send messages + + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md new file mode 100644 index 000000000..93700c383 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md @@ -0,0 +1,7 @@ +--- +type: docs +title: "Best practices with the Dapr AI .NET SDK client" +linkTitle: "Best Practices" +weight: 100000 +description: How to get up and running with the Dapr .NET SDK +--- \ No newline at end of file diff --git a/examples/AI/ConversationalAI/ConversationalAI.csproj b/examples/AI/ConversationalAI/ConversationalAI.csproj new file mode 100644 index 000000000..976265a5c --- /dev/null +++ b/examples/AI/ConversationalAI/ConversationalAI.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/AI/ConversationalAI/Program.cs b/examples/AI/ConversationalAI/Program.cs new file mode 100644 index 000000000..bd3dc906a --- /dev/null +++ b/examples/AI/ConversationalAI/Program.cs @@ -0,0 +1,23 @@ +using Dapr.AI.Conversation; +using Dapr.AI.Conversation.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprAiConversation(); + +var app = builder.Build(); + +var conversationClient = app.Services.GetRequiredService(); +var response = await conversationClient.ConverseAsync("conversation", + new List + { + new DaprConversationInput( + "Please write a witty haiku about the Dapr distributed programming framework at dapr.io", + DaprConversationRole.Generic) + }); + +Console.WriteLine("Received the following from the LLM:"); +foreach (var resp in response.Outputs) +{ + Console.WriteLine($"\t{resp.Result}"); +} diff --git a/src/Dapr.AI/AssemblyInfo.cs b/src/Dapr.AI/AssemblyInfo.cs new file mode 100644 index 000000000..8d96dcf56 --- /dev/null +++ b/src/Dapr.AI/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + diff --git a/src/Dapr.AI/Conversation/ConversationOptions.cs b/src/Dapr.AI/Conversation/ConversationOptions.cs new file mode 100644 index 000000000..87a49117a --- /dev/null +++ b/src/Dapr.AI/Conversation/ConversationOptions.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Google.Protobuf.WellKnownTypes; + +namespace Dapr.AI.Conversation; + +/// +/// Options used to configure the conversation operation. +/// +/// The identifier of the conversation this is a continuation of. +public sealed record ConversationOptions(string? ConversationId = null) +{ + /// + /// Temperature for the LLM to optimize for creativity or predictability. + /// + public double Temperature { get; init; } = default; + /// + /// Flag that indicates whether data that comes back from the LLM should be scrubbed of PII data. + /// + public bool ScrubPII { get; init; } = default; + /// + /// The metadata passing to the conversation components. + /// + public IReadOnlyDictionary Metadata { get; init; } = new Dictionary(); + /// + /// Parameters for all custom fields. + /// + public IReadOnlyDictionary Parameters { get; init; } = new Dictionary(); +} diff --git a/src/Dapr.AI/Conversation/DaprConversationClient.cs b/src/Dapr.AI/Conversation/DaprConversationClient.cs new file mode 100644 index 000000000..2335197bc --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationClient.cs @@ -0,0 +1,116 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Dapr.Common.Extensions; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.AI.Conversation; + +/// +/// Used to interact with the Dapr conversation building block. +/// +public sealed class DaprConversationClient : DaprAIClient +{ + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal P.Dapr.DaprClient Client { get; } + + /// + /// Used to initialize a new instance of a . + /// + /// The Dapr client. + /// The HTTP client used by the client for calling the Dapr runtime. + /// An optional token required to send requests to the Dapr sidecar. + public DaprConversationClient(P.Dapr.DaprClient client, + HttpClient httpClient, + string? daprApiToken = null) + { + this.Client = client; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; + } + + /// + /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar. + /// + /// The name of the Dapr conversation component. + /// The input values to send. + /// Optional options used to configure the conversation. + /// Cancellation token. + /// The response(s) provided by the LLM provider. + public override async Task ConverseAsync(string daprConversationComponentName, IReadOnlyList inputs, ConversationOptions? options = null, + CancellationToken cancellationToken = default) + { + var request = new P.ConversationRequest + { + Name = daprConversationComponentName + }; + + if (options is not null) + { + request.ContextID = options.ConversationId; + request.ScrubPII = options.ScrubPII; + + foreach (var (key, value) in options.Metadata) + { + request.Metadata.Add(key, value); + } + + foreach (var (key, value) in options.Parameters) + { + request.Parameters.Add(key, value); + } + } + + foreach (var input in inputs) + { + request.Inputs.Add(new P.ConversationInput + { + ScrubPII = input.ScrubPII, + Message = input.Message, + Role = input.Role.GetValueFromEnumMember() + }); + } + + var grpCCallOptions = + DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprConversationClient).Assembly, this.DaprApiToken, + cancellationToken); + + var result = await Client.ConverseAlpha1Async(request, grpCCallOptions).ConfigureAwait(false); + var outputs = result.Outputs.Select(output => new DaprConversationResult(output.Result) + { + Parameters = output.Parameters.ToDictionary(kvp => kvp.Key, parameter => parameter.Value) + }).ToList(); + + return new DaprConversationResponse(outputs); + } +} diff --git a/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs new file mode 100644 index 000000000..5e0a0825d --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationClientBuilder.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Microsoft.Extensions.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1.Dapr; + +namespace Dapr.AI.Conversation; + +/// +/// Used to create a new instance of a . +/// +public sealed class DaprConversationClientBuilder : DaprGenericClientBuilder +{ + /// + /// Used to initialize a new instance of the . + /// + /// + public DaprConversationClientBuilder(IConfiguration? configuration = null) : base(configuration) + { + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public override DaprConversationClient Build() + { + var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprConversationClient).Assembly); + var client = new Autogenerated.DaprClient(daprClientDependencies.channel); + return new DaprConversationClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.AI/Conversation/DaprConversationInput.cs b/src/Dapr.AI/Conversation/DaprConversationInput.cs new file mode 100644 index 000000000..3485849c8 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationInput.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.AI.Conversation; + +/// +/// Represents an input for the Dapr Conversational API. +/// +/// The message to send to the LLM. +/// The role indicating the entity providing the message. +/// If true, scrubs the data that goes into the LLM. +public sealed record DaprConversationInput(string Message, DaprConversationRole Role, bool ScrubPII = false); diff --git a/src/Dapr.AI/Conversation/DaprConversationResponse.cs b/src/Dapr.AI/Conversation/DaprConversationResponse.cs new file mode 100644 index 000000000..36de7fd6e --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationResponse.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.AI.Conversation; + +/// +/// The response for a conversation. +/// +/// The collection of conversation results. +/// The identifier of an existing or newly created conversation. +public record DaprConversationResponse(IReadOnlyList Outputs, string? ConversationId = null); diff --git a/src/Dapr.AI/Conversation/DaprConversationResult.cs b/src/Dapr.AI/Conversation/DaprConversationResult.cs new file mode 100644 index 000000000..700cc8730 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationResult.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Google.Protobuf.WellKnownTypes; + +namespace Dapr.AI.Conversation; + +/// +/// The result for a single conversational input. +/// +/// The result for one conversation input. +public record DaprConversationResult(string Result) +{ + /// + /// Parameters for all custom fields. + /// + public IReadOnlyDictionary Parameters { get; init; } = new Dictionary(); +} diff --git a/src/Dapr.AI/Conversation/DaprConversationRole.cs b/src/Dapr.AI/Conversation/DaprConversationRole.cs new file mode 100644 index 000000000..3e48a41c1 --- /dev/null +++ b/src/Dapr.AI/Conversation/DaprConversationRole.cs @@ -0,0 +1,42 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Dapr.Common.JsonConverters; + +namespace Dapr.AI.Conversation; + +/// +/// Represents who +/// +public enum DaprConversationRole +{ + /// + /// Represents a message sent by an AI. + /// + [EnumMember(Value="ai")] + AI, + /// + /// Represents a message sent by a human. + /// + [EnumMember(Value="human")] + Human, + /// + /// Represents a message sent by the system. + /// + [EnumMember(Value="system")] + System, + /// + /// Represents a message sent by a generic user. + /// + [EnumMember(Value="generic")] + Generic, + /// + /// Represents a message sent by a function. + /// + [EnumMember(Value="function")] + Function, + /// + /// Represents a message sent by a tool. + /// + [EnumMember(Value="tool")] + Tool +} diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs new file mode 100644 index 000000000..876d223b1 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilder.cs @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.AI.Conversation.Extensions; + +/// +/// Used by the fluent registration builder to configure a Dapr AI conversational manager. +/// +public sealed class DaprAiConversationBuilder : IDaprAiConversationBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } + + /// + /// Used to initialize a new . + /// + public DaprAiConversationBuilder(IServiceCollection services) + { + Services = services; + } +} diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs new file mode 100644 index 000000000..902fd82a3 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.AI.Conversation.Extensions; + +/// +/// Contains the dependency injection registration extensions for the Dapr AI Conversation operations. +/// +public static class DaprAiConversationBuilderExtensions +{ + /// + /// Registers the necessary functionality for the Dapr AI conversation functionality. + /// + /// + public static IDaprAiConversationBuilder AddDaprAiConversation(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.AddHttpClient(); + + var registration = new Func(provider => + { + var configuration = provider.GetService(); + var builder = new DaprConversationClientBuilder(configuration); + + var httpClientFactory = provider.GetRequiredService(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(provider, builder); + + return builder.Build(); + }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + break; + case ServiceLifetime.Singleton: + default: + services.TryAddSingleton(registration); + break; + } + + return new DaprAiConversationBuilder(services); + } +} diff --git a/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs new file mode 100644 index 000000000..30d3822d4 --- /dev/null +++ b/src/Dapr.AI/Conversation/Extensions/IDaprAiConversationBuilder.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.AI.Extensions; + +namespace Dapr.AI.Conversation.Extensions; + +/// +/// Provides a root builder for the Dapr AI conversational functionality facilitating a more fluent-style registration. +/// +public interface IDaprAiConversationBuilder : IDaprAiServiceBuilder +{ +} diff --git a/src/Dapr.AI/Dapr.AI.csproj b/src/Dapr.AI/Dapr.AI.csproj new file mode 100644 index 000000000..8220c5c4d --- /dev/null +++ b/src/Dapr.AI/Dapr.AI.csproj @@ -0,0 +1,26 @@ + + + + net6;net8 + enable + enable + Dapr.AI + Dapr AI SDK + Dapr AI SDK for performing operations associated with artificial intelligence. + alpha + + + + + + + + + + + + + + + + diff --git a/src/Dapr.AI/DaprAIClient.cs b/src/Dapr.AI/DaprAIClient.cs new file mode 100644 index 000000000..a2fd2255f --- /dev/null +++ b/src/Dapr.AI/DaprAIClient.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.AI.Conversation; + +namespace Dapr.AI; + +/// +/// The base implementation of a Dapr AI client. +/// +public abstract class DaprAIClient +{ + /// + /// Sends various inputs to the large language model via the Conversational building block on the Dapr sidecar. + /// + /// The name of the Dapr conversation component. + /// The input values to send. + /// Optional options used to configure the conversation. + /// Cancellation token. + /// The response(s) provided by the LLM provider. + public abstract Task ConverseAsync(string daprConversationComponentName, + IReadOnlyList inputs, ConversationOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs new file mode 100644 index 000000000..8a0a80c2c --- /dev/null +++ b/src/Dapr.AI/Extensions/IDaprAiServiceBuilder.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.AI.Extensions; + +/// +/// Responsible for registering Dapr AI service functionality. +/// +public interface IDaprAiServiceBuilder +{ + /// + /// The registered services on the builder. + /// + public IServiceCollection Services { get; } +} diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index c70aef77b..bd0bd1d01 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,2257 +11,2258 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client +using Dapr.Common.Extensions; + +namespace Dapr.Client; + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Grpc.Net.Client; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal class DaprClientGrpc : DaprClient { - using System; - using System.Buffers; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Net.Http; - using System.Net.Http.Json; - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using Google.Protobuf; - using Google.Protobuf.WellKnownTypes; - using Grpc.Core; - using Grpc.Net.Client; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + private const string AppIdKey = "appId"; + private const string MethodNameKey = "methodName"; - /// - /// A client for interacting with the Dapr endpoints. - /// - internal class DaprClientGrpc : DaprClient - { - private const string AppIdKey = "appId"; - private const string MethodNameKey = "methodName"; + private readonly Uri httpEndpoint; + private readonly HttpClient httpClient; - private readonly Uri httpEndpoint; - private readonly HttpClient httpClient; + private readonly JsonSerializerOptions jsonSerializerOptions; - private readonly JsonSerializerOptions jsonSerializerOptions; + private readonly GrpcChannel channel; + private readonly Autogenerated.Dapr.DaprClient client; + private readonly KeyValuePair? apiTokenHeader; - private readonly GrpcChannel channel; - private readonly Autogenerated.Dapr.DaprClient client; - private readonly KeyValuePair? apiTokenHeader; + // property exposed for testing purposes + internal Autogenerated.Dapr.DaprClient Client => client; - // property exposed for testing purposes - internal Autogenerated.Dapr.DaprClient Client => client; + public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; - public override JsonSerializerOptions JsonSerializerOptions => jsonSerializerOptions; + internal DaprClientGrpc( + GrpcChannel channel, + Autogenerated.Dapr.DaprClient inner, + HttpClient httpClient, + Uri httpEndpoint, + JsonSerializerOptions jsonSerializerOptions, + KeyValuePair? apiTokenHeader) + { + this.channel = channel; + this.client = inner; + this.httpClient = httpClient; + this.httpEndpoint = httpEndpoint; + this.jsonSerializerOptions = jsonSerializerOptions; + this.apiTokenHeader = apiTokenHeader; + + this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); + } - internal DaprClientGrpc( - GrpcChannel channel, - Autogenerated.Dapr.DaprClient inner, - HttpClient httpClient, - Uri httpEndpoint, - JsonSerializerOptions jsonSerializerOptions, - KeyValuePair? apiTokenHeader) - { - this.channel = channel; - this.client = inner; - this.httpClient = httpClient; - this.httpEndpoint = httpEndpoint; - this.jsonSerializerOptions = jsonSerializerOptions; - this.apiTokenHeader = apiTokenHeader; + #region Publish Apis + /// + public override Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(data, nameof(data)); - this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); - } + var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); + return MakePublishRequest(pubsubName, topicName, content, null, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + } - #region Publish Apis - /// - public override Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(data, nameof(data)); + public override Task PublishEventAsync( + string pubsubName, + string topicName, + TData data, + Dictionary metadata, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(data, nameof(data)); + ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); - var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, null, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); - } + var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); + return MakePublishRequest(pubsubName, topicName, content, metadata, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + } - public override Task PublishEventAsync( - string pubsubName, - string topicName, - TData data, - Dictionary metadata, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(data, nameof(data)); - ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); + /// + public override Task PublishEventAsync( + string pubsubName, + string topicName, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + return MakePublishRequest(pubsubName, topicName, null, null, null, cancellationToken); + } - var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, metadata, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); - } + public override Task PublishEventAsync( + string pubsubName, + string topicName, + Dictionary metadata, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); + return MakePublishRequest(pubsubName, topicName, null, metadata, null, cancellationToken); + } - /// - public override Task PublishEventAsync( - string pubsubName, - string topicName, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - return MakePublishRequest(pubsubName, topicName, null, null, null, cancellationToken); - } + public override Task PublishByteEventAsync( + string pubsubName, + string topicName, + ReadOnlyMemory data, + string dataContentType = Constants.ContentTypeApplicationJson, + Dictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken); + } - public override Task PublishEventAsync( - string pubsubName, - string topicName, - Dictionary metadata, - CancellationToken cancellationToken = default) + private async Task MakePublishRequest( + string pubsubName, + string topicName, + ByteString content, + Dictionary metadata, + string dataContentType, + CancellationToken cancellationToken) + { + var envelope = new Autogenerated.PublishEventRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); - return MakePublishRequest(pubsubName, topicName, null, metadata, null, cancellationToken); - } + PubsubName = pubsubName, + Topic = topicName, + }; - public override Task PublishByteEventAsync( - string pubsubName, - string topicName, - ReadOnlyMemory data, - string dataContentType = Constants.ContentTypeApplicationJson, - Dictionary metadata = default, - CancellationToken cancellationToken = default) + if (content != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken); + envelope.Data = content; + envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson; } - private async Task MakePublishRequest( - string pubsubName, - string topicName, - ByteString content, - Dictionary metadata, - string dataContentType, - CancellationToken cancellationToken) + if (metadata != null) { - var envelope = new Autogenerated.PublishEventRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; - - if (content != null) - { - envelope.Data = content; - envelope.DataContentType = dataContentType ?? Constants.ContentTypeApplicationJson; - } - - if (metadata != null) + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.PublishEventAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + try + { + await client.PublishEventAsync(envelope, options); } - - /// - public override Task> BulkPublishEventAsync( - string pubsubName, - string topicName, - IReadOnlyList events, - Dictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); - ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - ArgumentVerifier.ThrowIfNull(events, nameof(events)); - return MakeBulkPublishRequest(pubsubName, topicName, events, metadata, cancellationToken); + throw new DaprException("Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } + } + + /// + public override Task> BulkPublishEventAsync( + string pubsubName, + string topicName, + IReadOnlyList events, + Dictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); + ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); + ArgumentVerifier.ThrowIfNull(events, nameof(events)); + return MakeBulkPublishRequest(pubsubName, topicName, events, metadata, cancellationToken); + } - private async Task> MakeBulkPublishRequest( - string pubsubName, - string topicName, - IReadOnlyList events, - Dictionary metadata, - CancellationToken cancellationToken) - { - var envelope = new Autogenerated.BulkPublishRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; + private async Task> MakeBulkPublishRequest( + string pubsubName, + string topicName, + IReadOnlyList events, + Dictionary metadata, + CancellationToken cancellationToken) + { + var envelope = new Autogenerated.BulkPublishRequest() + { + PubsubName = pubsubName, + Topic = topicName, + }; - Dictionary> entryMap = new Dictionary>(); + Dictionary> entryMap = new Dictionary>(); - for (int counter = 0; counter < events.Count; counter++) + for (int counter = 0; counter < events.Count; counter++) + { + var entry = new Autogenerated.BulkPublishRequestEntry() { - var entry = new Autogenerated.BulkPublishRequestEntry() - { - EntryId = counter.ToString(), - Event = TypeConverters.ToJsonByteString(events[counter], this.jsonSerializerOptions), - ContentType = events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, - Metadata = {}, - }; - envelope.Entries.Add(entry); - entryMap.Add(counter.ToString(), new BulkPublishEntry( - entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); - } + EntryId = counter.ToString(), + Event = TypeConverters.ToJsonByteString(events[counter], this.jsonSerializerOptions), + ContentType = events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, + Metadata = {}, + }; + envelope.Entries.Add(entry); + entryMap.Add(counter.ToString(), new BulkPublishEntry( + entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); + } - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await client.BulkPublishEventAlpha1Async(envelope, options); + try + { + var response = await client.BulkPublishEventAlpha1Async(envelope, options); - List> failedEntries = new List>(); + List> failedEntries = new List>(); - foreach (var entry in response.FailedEntries) - { - BulkPublishResponseFailedEntry domainEntry = new BulkPublishResponseFailedEntry( - entryMap[entry.EntryId], entry.Error); - failedEntries.Add(domainEntry); - } - - var bulkPublishResponse = new BulkPublishResponse(failedEntries); - - return bulkPublishResponse; - } - catch (RpcException ex) + foreach (var entry in response.FailedEntries) { - throw new DaprException("Bulk Publish operation failed: the Dapr endpoint indicated a " + - "failure. See InnerException for details.", ex); + BulkPublishResponseFailedEntry domainEntry = new BulkPublishResponseFailedEntry( + entryMap[entry.EntryId], entry.Error); + failedEntries.Add(domainEntry); } + + var bulkPublishResponse = new BulkPublishResponse(failedEntries); + + return bulkPublishResponse; + } + catch (RpcException ex) + { + throw new DaprException("Bulk Publish operation failed: the Dapr endpoint indicated a " + + "failure. See InnerException for details.", ex); } - #endregion + } + #endregion - #region InvokeBinding Apis + #region InvokeBinding Apis - /// - public override async Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); - ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + /// + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + } + + /// + public override async Task InvokeBindingAsync( + string bindingName, + string operation, + TRequest data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); + ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); - _ = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); + var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + + try + { + return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); } + catch (JsonException ex) + { + throw new DaprException("Binding operation failed: the response payload could not be deserialized. See InnerException for details.", ex); + } + } - /// - public override async Task InvokeBindingAsync( - string bindingName, - string operation, - TRequest data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default) + { + var bytes = ByteString.CopyFrom(request.Data.Span); + var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, request.Metadata, cancellationToken); + return new BindingResponse(request, response.Data.Memory, response.Metadata); + } + + private async Task MakeInvokeBindingRequestAsync( + string name, + string operation, + ByteString data, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.InvokeBindingRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(bindingName, nameof(bindingName)); - ArgumentVerifier.ThrowIfNullOrEmpty(operation, nameof(operation)); + Name = name, + Operation = operation + }; - var bytes = TypeConverters.ToJsonByteString(data, this.jsonSerializerOptions); - var response = await MakeInvokeBindingRequestAsync(bindingName, operation, bytes, metadata, cancellationToken); + if (data != null) + { + envelope.Data = data; + } - try - { - return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); - } - catch (JsonException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("Binding operation failed: the response payload could not be deserialized. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } } - public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try { - var bytes = ByteString.CopyFrom(request.Data.Span); - var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, request.Metadata, cancellationToken); - return new BindingResponse(request, response.Data.Memory, response.Metadata); + return await client.InvokeBindingAsync(envelope, options); } - - private async Task MakeInvokeBindingRequestAsync( - string name, - string operation, - ByteString data, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + catch (RpcException ex) { - var envelope = new Autogenerated.InvokeBindingRequest() - { - Name = name, - Operation = operation - }; + throw new DaprException("Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + #endregion - if (data != null) - { - envelope.Data = data; - } + #region InvokeMethod Apis - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + /// + /// 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>()); + } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - return await client.InvokeBindingAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - #endregion - - #region InvokeMethod Apis - - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the HTTP method specified by . - /// - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// An for use with SendInvokeMethodRequestAsync. - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) - { - return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); - } - - /// - /// Creates an that can be used to perform service invocation for the - /// application identified by and invokes the method specified by - /// with the HTTP method specified by . - /// - /// The to use for the invocation request. - /// The Dapr application id to invoke the method on. - /// The name of the method to invoke. - /// A collection of key/value pairs to populate the query string from. - /// An for use with SendInvokeMethodRequestAsync. - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, - IReadOnlyCollection> queryStringParameters) - { - ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - - // Note about this, it's possible to construct invalid stuff using path navigation operators - // like `../..`. But the principle of garbage in -> garbage out holds. - // - // This approach avoids some common pitfalls that could lead to undesired encoding. - var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); - var request = new HttpRequestMessage(httpMethod, requestUri); + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters) + { + ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); + + // Note about this, it's possible to construct invalid stuff using path navigation operators + // like `../..`. But the principle of garbage in -> garbage out holds. + // + // This approach avoids some common pitfalls that could lead to undesired encoding. + var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; + var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); + var request = new HttpRequestMessage(httpMethod, requestUri); - request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); - request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); - - if (this.apiTokenHeader is not null) - { - request.Headers.TryAddWithoutValidation(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); + request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); - return request; + if (this.apiTokenHeader is not null) + { + request.Headers.TryAddWithoutValidation(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - /// 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)); + return request; + } - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); - request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); - return request; - } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by and a JSON serialized request body specified by + /// . + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters, TRequest data) + { + ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); + request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); + return request; + } + + public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); + + if (!this.httpEndpoint.IsBaseOf(request.RequestUri)) { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI."); + } - if (!this.httpEndpoint.IsBaseOf(request.RequestUri)) - { - throw new InvalidOperationException("The provided request URI is not a Dapr service invocation URI."); - } + // Note: we intentionally DO NOT validate the status code here. + // This method allows you to 'invoke' without exceptions on non-2xx. + try + { + return await this.httpClient.SendAsync(request, cancellationToken); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - // Note: we intentionally DO NOT validate the status code here. - // This method allows you to 'invoke' without exceptions on non-2xx. - try - { - return await this.httpClient.SendAsync(request, cancellationToken); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: null); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: null); } + } - /// - /// - /// 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. - /// - /// + /// + /// + /// Creates an that can be used to perform Dapr service invocation using + /// objects. + /// + /// + /// The client will read the property, and + /// interpret the hostname as the destination app-id. The + /// property will be replaced with a new URI with the authority section replaced by the instance's value + /// and the path portion of the URI rewritten to follow the format of a Dapr service invocation request. + /// + /// + /// + /// An optional app-id. If specified, the app-id will be configured as the value of + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. + /// + /// An that can be used to perform service invocation requests. + /// + /// #nullable enable - public override HttpClient CreateInvokableHttpClient(string? appId = null) => - DaprClient.CreateInvokeHttpClient(appId, this.httpEndpoint?.AbsoluteUri, this.apiTokenHeader?.Value); - #nullable disable + public override 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) + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); + + var response = await InvokeMethodWithResponseAsync(request, cancellationToken); + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - var response = await InvokeMethodWithResponseAsync(request, cancellationToken); - try - { - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } + } - public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNull(request, nameof(request)); + public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(request, nameof(request)); - var response = await InvokeMethodWithResponseAsync(request, cancellationToken); - try - { - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + var response = await InvokeMethodWithResponseAsync(request, cancellationToken); + try + { + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - try - { - return await response.Content.ReadFromJsonAsync(this.jsonSerializerOptions, cancellationToken); - } - catch (HttpRequestException ex) - { - // Our code path for creating requests places these keys in the request properties. We don't want to fail - // if they are not present. - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } - catch (JsonException ex) - { - request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); - request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - - throw new InvocationException( - appId: appId as string, - methodName: methodName as string, - innerException: ex, - response: response); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + try { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - - var envelope = new Autogenerated.InvokeServiceRequest() - { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, - }; + return await response.Content.ReadFromJsonAsync(this.jsonSerializerOptions, cancellationToken); + } + catch (HttpRequestException ex) + { + // Our code path for creating requests places these keys in the request properties. We don't want to fail + // if they are not present. + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - var options = CreateCallOptions(headers: null, cancellationToken); + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); + } + catch (JsonException ex) + { + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); - try - { - _ = await this.Client.InvokeServiceAsync(envelope, options); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + throw new InvocationException( + appId: appId as string, + methodName: methodName as string, + innerException: ex, + response: response); } + } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - var envelope = new Autogenerated.InvokeServiceRequest() + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), - }, - }; + Method = methodName, + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - _ = await this.Client.InvokeServiceAsync(envelope, options); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + _ = await this.Client.InvokeServiceAsync(envelope, options); } - - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + throw new InvocationException(appId, methodName, ex); + } + } - var envelope = new Autogenerated.InvokeServiceRequest() + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, - }; + Method = methodName, + ContentType = Constants.ContentTypeApplicationGrpc, + Data = Any.Pack(data), + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await this.Client.InvokeServiceAsync(envelope, options); - return response.Data.Unpack(); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + _ = await this.Client.InvokeServiceAsync(envelope, options); } - - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); - ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); + throw new InvocationException(appId, methodName, ex); + } + } + + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - var envelope = new Autogenerated.InvokeServiceRequest() + var envelope = new Autogenerated.InvokeServiceRequest() + { + Id = appId, + Message = new Autogenerated.InvokeRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), - }, - }; + Method = methodName, + }, + }; - var options = CreateCallOptions(headers: null, cancellationToken); + var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await this.Client.InvokeServiceAsync(envelope, options); - return response.Data.Unpack(); - } - catch (RpcException ex) - { - throw new InvocationException(appId, methodName, ex); - } + try + { + var response = await this.Client.InvokeServiceAsync(envelope, options); + return response.Data.Unpack(); } + catch (RpcException ex) + { + throw new InvocationException(appId, methodName, ex); + } + } - #endregion - - #region State Apis + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); + ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); - /// - public override async Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + var envelope = new Autogenerated.InvokeServiceRequest() { - var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); - - var bulkResponse = new List(); - foreach (var item in rawBulkState) + Id = appId, + Message = new Autogenerated.InvokeRequest() { - bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); - } + Method = methodName, + ContentType = Constants.ContentTypeApplicationGrpc, + Data = Any.Pack(data), + }, + }; - 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)); - } + var options = CreateCallOptions(headers: null, cancellationToken); - return bulkResponse; + try + { + var response = await this.Client.InvokeServiceAsync(envelope, options); + return response.Data.Unpack(); } - - /// - /// 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) + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - if (keys.Count == 0) - throw new ArgumentException("keys do not contain any elements"); - - var envelope = new Autogenerated.GetBulkStateRequest() - { - StoreName = storeName, - Parallelism = parallelism ?? default - }; + throw new InvocationException(appId, methodName, ex); + } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + #endregion - envelope.Keys.AddRange(keys); + #region State Apis - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetBulkStateResponse response; + /// + 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); - try - { - response = await client.GetBulkStateAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException( - "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", - ex); - } + var bulkResponse = new List(); + foreach (var item in rawBulkState) + { + bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); + } - var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); - foreach (var item in response.Items) - { - bulkResponse.Add((item.Key, item.Etag, item.Data)); - } + 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); - return bulkResponse; + 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)); } - - /// - public override async Task GetStateAsync( - string storeName, - string key, - ConsistencyMode? consistencyMode = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - var envelope = new Autogenerated.GetStateRequest() - { - StoreName = storeName, - Key = key, - }; - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } + return bulkResponse; + } - if (consistencyMode != null) - { - envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); - } + /// + /// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling + /// to the public callers of this method to avoid duplicate deserialization. + /// + private async Task> GetBulkStateRawAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + if (keys.Count == 0) + throw new ArgumentException("keys do not contain any elements"); - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetStateResponse response; + var envelope = new Autogenerated.GetBulkStateRequest() + { + StoreName = storeName, + Parallelism = parallelism ?? default + }; - try - { - response = await client.GetStateAsync(envelope, options); - } - catch (RpcException ex) + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - try - { - return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + envelope.Keys.AddRange(keys); + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetBulkStateResponse response; + + try + { + response = await client.GetBulkStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } - /// - public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); + foreach (var item in response.Items) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + bulkResponse.Add((item.Key, item.Etag, item.Data)); + } - if (items.Count == 0) - { - throw new ArgumentException("items do not contain any elements"); - } + return bulkResponse; + } + + /// + public override async Task GetStateAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + var envelope = new Autogenerated.GetStateRequest() + { + StoreName = storeName, + Key = key, + }; - foreach (var item in items) + if (metadata != null) + { + foreach (var kvp in metadata) { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; - - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } - if (item.StateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); - } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; - if (item.Value != null) - { - stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions); - } + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - envelope.States.Add(stateItem); - } + try + { + return TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + } + } - try - { - await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken); - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + if (items.Count == 0) + { + throw new ArgumentException("items do not contain any elements"); } - /// - public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + var envelope = new Autogenerated.SaveStateRequest() + { + StoreName = storeName, + }; + + foreach (var item in items) { - var envelope = new Autogenerated.DeleteBulkStateRequest() + var stateItem = new Autogenerated.StateItem() { - StoreName = storeName, + Key = item.Key, }; - foreach (var item in items) + if (item.ETag != null) { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; - - if (item.ETag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; - } - - if (item.Metadata != null) - { - foreach (var kvp in item.Metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; + } - if (item.StateOptions != null) + if (item.Metadata != null) + { + foreach (var kvp in item.Metadata) { - stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); + stateItem.Metadata.Add(kvp.Key, kvp.Value); } - - envelope.States.Add(stateItem); } - try + if (item.StateOptions != null) { - await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); + stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); } - catch (RpcException ex) + + if (item.Value != null) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateItem.Value = TypeConverters.ToJsonByteString(item.Value, this.jsonSerializerOptions); } + envelope.States.Add(stateItem); + } + + try + { + await this.Client.SaveStateAsync(envelope, cancellationToken: cancellationToken); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - /// - public override async Task<(TValue value, string etag)> GetStateAndETagAsync( - string storeName, - string key, - ConsistencyMode? consistencyMode = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + } + + /// + public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.DeleteBulkStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + StoreName = storeName, + }; - var envelope = new Autogenerated.GetStateRequest() + foreach (var item in items) + { + var stateItem = new Autogenerated.StateItem() { - StoreName = storeName, - Key = key + Key = item.Key, }; - if (metadata != null) + if (item.ETag != null) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + stateItem.Etag = new Autogenerated.Etag() { Value = item.ETag }; } - if (consistencyMode != null) + if (item.Metadata != null) { - envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + foreach (var kvp in item.Metadata) + { + stateItem.Metadata.Add(kvp.Key, kvp.Value); + } } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetStateResponse response; - - try - { - response = await client.GetStateAsync(envelope, options); - } - catch (RpcException ex) + if (item.StateOptions != null) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + stateItem.Options = ToAutoGeneratedStateOptions(item.StateOptions); } - try - { - return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + envelope.States.Add(stateItem); } - /// - public override async Task SaveStateAsync( - string storeName, - string key, - TValue value, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeSaveStateCallAsync( - storeName, - key, - value, - etag: null, - stateOptions, - metadata, - cancellationToken); + try + { + await this.Client.DeleteBulkStateAsync(envelope, cancellationToken: cancellationToken); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - /// - public override async Task TrySaveStateAsync( - string storeName, - string key, - TValue value, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + } + + /// + public override async Task<(TValue value, string etag)> GetStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + var envelope = new Autogenerated.GetStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + StoreName = storeName, + Key = key + }; - return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + if (metadata != null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } } - private async Task MakeSaveStateCallAsync( - string storeName, - string key, - TValue value, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (consistencyMode != null) { - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; - var stateItem = new Autogenerated.StateItem() - { - Key = key, - }; + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - stateItem.Metadata.Add(kvp.Key, kvp.Value); - } - } + try + { + return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + } + } - if (etag != null) - { - stateItem.Etag = new Autogenerated.Etag() { Value = etag }; - } + /// + public override async Task SaveStateAsync( + string storeName, + string key, + TValue value, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeSaveStateCallAsync( + storeName, + key, + value, + etag: null, + stateOptions, + metadata, + cancellationToken); + } - if (stateOptions != null) - { - stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); - } + /// + public override async Task TrySaveStateAsync( + string storeName, + string key, + TValue value, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); - if (value != null) - { - stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); - } + return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + } - envelope.States.Add(stateItem); + private async Task MakeSaveStateCallAsync( + string storeName, + string key, + TValue value, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.SaveStateRequest() + { + StoreName = storeName, + }; - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.SaveStateAsync(envelope, options); - return true; - } - catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) - { - // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like - // the right status code at first, but check the docs, it fits this use-case. - // - // When an ETag is used we surface this though the Try... pattern - return false; - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } + var stateItem = new Autogenerated.StateItem() + { + Key = key, + }; - /// - public override async Task ExecuteStateTransactionAsync( - string storeName, - IReadOnlyList operations, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (metadata != null) { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); - if (operations.Count == 0) + foreach (var kvp in metadata) { - throw new ArgumentException($"{nameof(operations)} does not contain any elements"); + stateItem.Metadata.Add(kvp.Key, kvp.Value); } + } - await this.MakeExecuteStateTransactionCallAsync( - storeName, - operations, - metadata, - cancellationToken); + if (etag != null) + { + stateItem.Etag = new Autogenerated.Etag() { Value = etag }; } - private async Task MakeExecuteStateTransactionCallAsync( - string storeName, - IReadOnlyList states, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (stateOptions != null) { - var envelope = new Autogenerated.ExecuteStateTransactionRequest() - { - StoreName = storeName, - }; + stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); + } - foreach (var state in states) - { - var stateOperation = new Autogenerated.TransactionalStateOperation - { - OperationType = state.OperationType.ToString().ToLower(), - Request = ToAutogeneratedStateItem(state) - }; + if (value != null) + { + stateItem.Value = TypeConverters.ToJsonByteString(value, this.jsonSerializerOptions); + } - envelope.Operations.Add(stateOperation); + envelope.States.Add(stateItem); - } + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + await client.SaveStateAsync(envelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - // Add metadata that applies to all operations if specified - if (metadata != null) - { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } - } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.ExecuteStateTransactionAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + public override async Task ExecuteStateTransactionAsync( + string storeName, + IReadOnlyList operations, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNull(operations, nameof(operations)); + if (operations.Count == 0) + { + throw new ArgumentException($"{nameof(operations)} does not contain any elements"); } - private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + await this.MakeExecuteStateTransactionCallAsync( + storeName, + operations, + metadata, + cancellationToken); + } + + private async Task MakeExecuteStateTransactionCallAsync( + string storeName, + IReadOnlyList states, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.ExecuteStateTransactionRequest() { - var stateOperation = new Autogenerated.StateItem - { - Key = state.Key - }; + StoreName = storeName, + }; - if (state.Value != null) + foreach (var state in states) + { + var stateOperation = new Autogenerated.TransactionalStateOperation { - stateOperation.Value = ByteString.CopyFrom(state.Value); - } + OperationType = state.OperationType.ToString().ToLower(), + Request = ToAutogeneratedStateItem(state) + }; - if (state.ETag != null) - { - stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; - } + envelope.Operations.Add(stateOperation); - if (state.Metadata != null) - { - foreach (var kvp in state.Metadata) - { - stateOperation.Metadata.Add(kvp.Key, kvp.Value); - } - } + } - if (state.Options != null) + // Add metadata that applies to all operations if specified + if (metadata != null) + { + foreach (var kvp in metadata) { - stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + envelope.Metadata.Add(kvp.Key, kvp.Value); } - - return stateOperation; } - - /// - public override async Task DeleteStateAsync( - string storeName, - string key, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var options = CreateCallOptions(headers: null, cancellationToken); + try { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - - _ = await this.MakeDeleteStateCallAsync( - storeName, - key, - etag: null, - stateOptions, - metadata, - cancellationToken); + await client.ExecuteStateTransactionAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } + } - /// - public override async Task TryDeleteStateAsync( - string storeName, - string key, - string etag, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) + { + var stateOperation = new Autogenerated.StateItem { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and - // rely on bubbling up the error if any from Dapr runtime - ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + Key = state.Key + }; - return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + if (state.Value != null) + { + stateOperation.Value = ByteString.CopyFrom(state.Value); } - private async Task MakeDeleteStateCallAsync( - string storeName, - string key, - string etag = default, - StateOptions stateOptions = default, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + if (state.ETag != null) { - var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() - { - StoreName = storeName, - Key = key, - }; + stateOperation.Etag = new Autogenerated.Etag() { Value = state.ETag }; + } - if (metadata != null) + if (state.Metadata != null) + { + foreach (var kvp in state.Metadata) { - foreach (var kvp in metadata) - { - deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); - } + stateOperation.Metadata.Add(kvp.Key, kvp.Value); } + } - if (etag != null) - { - deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; - } + if (state.Options != null) + { + stateOperation.Options = ToAutoGeneratedStateOptions(state.Options); + } - if (stateOptions != null) - { - deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); - } + return stateOperation; + } - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - await client.DeleteStateAsync(deleteStateEnvelope, options); - return true; - } - catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) - { - // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like - // the right status code at first, but check the docs, it fits this use-case. - // - // When an ETag is used we surface this though the Try... pattern - return false; - } - catch (RpcException ex) - { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } + /// + public override async Task DeleteStateAsync( + string storeName, + string key, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + _ = await this.MakeDeleteStateCallAsync( + storeName, + key, + etag: null, + stateOptions, + metadata, + cancellationToken); + } + + /// + public override async Task TryDeleteStateAsync( + string storeName, + string key, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + // Not all state stores treat empty etag as invalid. Therefore, we will not verify an empty etag and + // rely on bubbling up the error if any from Dapr runtime + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + + return await this.MakeDeleteStateCallAsync(storeName, key, etag, stateOptions, metadata, cancellationToken); + } - /// - public async override Task> QueryStateAsync( - string storeName, - string jsonQuery, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + private async Task MakeDeleteStateCallAsync( + string storeName, + string key, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() { - var queryRequest = new Autogenerated.QueryStateRequest() - { - StoreName = storeName, - Query = jsonQuery - }; + StoreName = storeName, + Key = key, + }; - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - queryRequest.Metadata.Add(kvp.Key, kvp.Value); - } + deleteStateEnvelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); + if (etag != null) + { + deleteStateEnvelope.Etag = new Autogenerated.Etag() { Value = etag }; + } - try - { - var items = new List>(); - var failedKeys = new List(); - var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); - foreach (var item in queryResponse.Results) - { - if (!string.IsNullOrEmpty(item.Error)) - { - // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. - failedKeys.Add(item.Key); - continue; - } - items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); - } + if (stateOptions != null) + { + deleteStateEnvelope.Options = ToAutoGeneratedStateOptions(stateOptions); + } - var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); - if (failedKeys.Count > 0) - { - // We encountered some bad keys so we throw instead of returning to alert the user. - throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); - } + var options = CreateCallOptions(headers: null, cancellationToken); - return results; - } - catch (RpcException ex) - { - throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); - } - catch (JsonException ex) - { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); - } + try + { + await client.DeleteStateAsync(deleteStateEnvelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; + } + catch (RpcException ex) + { + throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion + } - #region Secret Apis - /// - public async override Task> GetSecretAsync( - string storeName, - string key, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + /// + public async override Task> QueryStateAsync( + string storeName, + string jsonQuery, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var queryRequest = new Autogenerated.QueryStateRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + StoreName = storeName, + Query = jsonQuery + }; - var envelope = new Autogenerated.GetSecretRequest() + if (metadata != null) + { + foreach (var kvp in metadata) { - StoreName = storeName, - Key = key - }; + queryRequest.Metadata.Add(kvp.Key, kvp.Value); + } + } - if (metadata != null) + var options = CreateCallOptions(headers: null, cancellationToken); + + try + { + var items = new List>(); + var failedKeys = new List(); + var queryResponse = await client.QueryStateAlpha1Async(queryRequest, options); + foreach (var item in queryResponse.Results) { - foreach (var kvp in metadata) + if (!string.IsNullOrEmpty(item.Error)) { - envelope.Metadata.Add(kvp.Key, kvp.Value); + // When we encounter an error, we record the key and prepare to throw an exception at the end of the results. + failedKeys.Add(item.Key); + continue; } + items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetSecretResponse response; - - try - { - response = await client.GetSecretAsync(envelope, options); - } - catch (RpcException ex) + var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); + if (failedKeys.Count > 0) { - throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + // We encountered some bad keys so we throw instead of returning to alert the user. + throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); } - return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + return results; + } + catch (RpcException ex) + { + throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); + } + catch (JsonException ex) + { + throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); } + } + #endregion + + #region Secret Apis + /// + public async override Task> GetSecretAsync( + string storeName, + string key, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - /// - public async override Task>> GetBulkSecretAsync( - string storeName, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + var envelope = new Autogenerated.GetSecretRequest() { - var envelope = new Autogenerated.GetBulkSecretRequest() - { - StoreName = storeName - }; + StoreName = storeName, + Key = key + }; - if (metadata != null) + if (metadata != null) + { + foreach (var kvp in metadata) { - foreach (var kvp in metadata) - { - envelope.Metadata.Add(kvp.Key, kvp.Value); - } + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetBulkSecretResponse response; + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetSecretResponse response; - try - { - response = await client.GetBulkSecretAsync(envelope, options); - } - catch (RpcException ex) + try + { + response = await client.GetSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); + } + + /// + public async override Task>> GetBulkSecretAsync( + string storeName, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.GetBulkSecretRequest() + { + StoreName = storeName + }; + + if (metadata != null) + { + foreach (var kvp in metadata) { - throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + envelope.Metadata.Add(kvp.Key, kvp.Value); } + } + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetBulkSecretResponse response; - return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + try + { + response = await client.GetBulkSecretAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion - #region Configuration API - /// - public async override Task GetConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); + } + #endregion + + #region Configuration API + /// + public async override Task GetConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + + var request = new Autogenerated.GetConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + StoreName = storeName + }; - var request = new Autogenerated.GetConfigurationRequest() - { - StoreName = storeName - }; + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (keys != null && keys.Count > 0) + if (metadata != null) + { + foreach (var kvp in metadata) { - request.Keys.AddRange(keys); + request.Metadata.Add(kvp.Key, kvp.Value); } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } - } + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); + try + { + response = await client.GetConfigurationAsync(request, options); + } + catch (RpcException ex) + { + throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.GetConfigurationResponse response = new Autogenerated.GetConfigurationResponse(); - try - { - response = await client.GetConfigurationAsync(request, options); - } - catch (RpcException ex) - { - throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); - var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); + return new GetConfigurationResponse(responseItems); + } - return new GetConfigurationResponse(responseItems); - } + /// + public override Task SubscribeConfiguration( + string storeName, + IReadOnlyList keys, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - /// - public override Task SubscribeConfiguration( - string storeName, - IReadOnlyList keys, - IReadOnlyDictionary metadata = default, - CancellationToken cancellationToken = default) + Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + StoreName = storeName + }; - Autogenerated.SubscribeConfigurationRequest request = new Autogenerated.SubscribeConfigurationRequest() - { - StoreName = storeName - }; + if (keys != null && keys.Count > 0) + { + request.Keys.AddRange(keys); + } - if (keys != null && keys.Count > 0) + if (metadata != null) + { + foreach (var kvp in metadata) { - request.Keys.AddRange(keys); + request.Metadata.Add(kvp.Key, kvp.Value); } + } - if (metadata != null) - { - foreach (var kvp in metadata) - { - request.Metadata.Add(kvp.Key, kvp.Value); - } - } + var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); + return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); + } - var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); - return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); - } + public override async Task UnsubscribeConfiguration( + string storeName, + string id, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - public override async Task UnsubscribeConfiguration( - string storeName, - string id, - CancellationToken cancellationToken = default) + Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - - Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() - { - StoreName = storeName, - Id = id - }; + StoreName = storeName, + Id = id + }; - var options = CreateCallOptions(headers: null, cancellationToken); - var resp = await client.UnsubscribeConfigurationAsync(request, options); - return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); - } + var options = CreateCallOptions(headers: null, cancellationToken); + var resp = await client.UnsubscribeConfigurationAsync(request, options); + return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); + } - #endregion + #endregion - #region Cryptography + #region Cryptography - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> EncryptAsync(string vaultResourceName, - ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, - CancellationToken cancellationToken = default) + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) { - if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) - { - var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, - cancellationToken); + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + cancellationToken); - var bufferedResult = new ArrayBufferWriter(); + var bufferedResult = new ArrayBufferWriter(); - await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) - { - bufferedResult.Write(item.Span); - } - - return bufferedResult.WrittenMemory; + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); } - - throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + + return bufferedResult.WrittenMemory; } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, - string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, + string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + + var encryptRequestOptions = new Autogenerated.EncryptRequestOptions + { + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = shouldOmitDecryptionKeyName + }; + + if (!shouldOmitDecryptionKeyName) { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); - ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; + } - var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.EncryptAlpha1(options); - var encryptRequestOptions = new Autogenerated.EncryptRequestOptions - { - ComponentName = vaultResourceName, - DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), - KeyName = keyName, - KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), - OmitDecryptionKeyName = shouldOmitDecryptionKeyName - }; + //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); + } - if (!shouldOmitDecryptionKeyName) - { - ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); - encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; - } + /// + /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. + /// + private async Task SendPlaintextStreamAsync(Stream plaintextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.EncryptRequestOptions encryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the encryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.EncryptAlpha1(options); - - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( - //Stream the plaintext data to the sidecar in chunks - SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, - duplexStream, encryptRequestOptions, cancellationToken), - //At the same time, retrieve the encrypted response from the sidecar - receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); - } - - /// - /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. - /// - private async Task SendPlaintextStreamAsync(Stream plaintextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.EncryptRequestOptions encryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the encryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); - - //Send the plaintext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) - { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; + //Send the plaintext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != - 0) - { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != + 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); - - //Increment the sequence number - sequenceNumber++; - } - } + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); + //Increment the sequence number + sequenceNumber++; + } } - /// - /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return encryptResponse.Payload.Data.Memory; - } + yield return encryptResponse.Payload.Data.Memory; } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, - DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); - ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, + DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); - var decryptRequestOptions = new Autogenerated.DecryptRequestOptions - { - ComponentName = vaultResourceName, - KeyName = keyName - }; + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, + KeyName = keyName + }; - var options = CreateCallOptions(headers: null, cancellationToken); - var duplexStream = client.DecryptAlpha1(options); + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); - //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( //Stream the ciphertext data to the sidecar in chunks SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes, duplexStream, decryptRequestOptions, cancellationToken), //At the same time, retrieve the decrypted response from the sidecar receiveResult) - //Return only the result of the `RetrieveEncryptedStreamAsync` method + //Return only the result of the `RetrieveEncryptedStreamAsync` method .ContinueWith(t => receiveResult.Result, cancellationToken); - } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override Task>> DecryptAsync(string vaultResourceName, - Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => - DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), - cancellationToken); + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); - /// - /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. - /// - private async Task SendCiphertextStreamAsync(Stream ciphertextStream, - int streamingBlockSizeInBytes, - AsyncDuplexStreamingCall duplexStream, - Autogenerated.DecryptRequestOptions decryptRequestOptions, - CancellationToken cancellationToken) - { - //Start with passing the metadata about the decryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions decryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the decryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); - //Send the ciphertext bytes in blocks in subsequent messages - await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) - { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; + //Send the ciphertext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; - while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + { + await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest { - await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest + Payload = new Autogenerated.StreamPayload { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), - Seq = sequenceNumber - } - }, cancellationToken); + Data = ByteString.CopyFrom(buffer, 0, bytesRead), + Seq = sequenceNumber + } + }, cancellationToken); - //Increment the sequence number - sequenceNumber++; - } + //Increment the sequence number + sequenceNumber++; } - - //Send the completion message - await duplexStream.RequestStream.CompleteAsync(); } + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } - /// - /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. - /// - private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( - AsyncDuplexStreamingCall duplexStream, - [EnumeratorCancellation] CancellationToken cancellationToken) + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) { - await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) - .ConfigureAwait(false)) - { - yield return decryptResponse.Payload.Data.Memory; - } + yield return decryptResponse.Payload.Data.Memory; } + } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, - CancellationToken cancellationToken = default) + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) { - if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) - { - var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), - keyName, decryptionOptions, 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); - } - - return bufferedResult.WrittenMemory; + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); } - throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + return bufferedResult.WrittenMemory; } - /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task> DecryptAsync(string vaultResourceName, - ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => - await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, - new DecryptionOptions(), cancellationToken); - - #region Subtle Crypto Implementation - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, - // CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleGetKeyRequest() - // { - // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleGetKeyResponse response; - - // try - // { - // response = await client.SubtleGetKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); - // } - - // return (response.Name, response.PublicKey); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, - // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleEncryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Plaintext = ByteString.CopyFrom(plainTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleEncryptResponse response; - - // try - // { - // response = await client.SubtleEncryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", - // ex); - // } - - // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, - // byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleDecryptRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Ciphertext = ByteString.CopyFrom(cipherTextBytes), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleDecryptResponse response; - - // try - // { - // response = await client.SubtleDecryptAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); - // } - - // return response.Plaintext.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, - // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - - // var envelope = new Autogenerated.SubtleWrapKeyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // PlaintextKey = ByteString.CopyFrom(plainTextKey), - // AssociatedData = ByteString.CopyFrom(associatedData) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleWrapKeyResponse response; - - // try - // { - // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, - // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleUnwrapKeyRequest - // { - // ComponentName = vaultResourceName, - // WrappedKey = ByteString.CopyFrom(wrappedKey), - // AssociatedData = ByteString.CopyFrom(associatedData), - // Algorithm = algorithm, - // KeyName = keyName, - // Nonce = ByteString.CopyFrom(nonce), - // Tag = ByteString.CopyFrom(tag) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleUnwrapKeyResponse response; - - // try - // { - // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.PlaintextKey.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleSignRequest - // { - // ComponentName = vaultResourceName, - // Digest = ByteString.CopyFrom(digest), - // Algorithm = algorithm, - // KeyName = keyName - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleSignResponse response; - - // try - // { - // response = await client.SubtleSignAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Signature.ToByteArray(); - //} - - ///// - //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, - // string algorithm, string keyName, CancellationToken cancellationToken = default) - //{ - // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); - // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); - // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); - - // var envelope = new Autogenerated.SubtleVerifyRequest - // { - // ComponentName = vaultResourceName, - // Algorithm = algorithm, - // KeyName = keyName, - // Signature = ByteString.CopyFrom(signature), - // Digest = ByteString.CopyFrom(digest) - // }; - - // var options = CreateCallOptions(headers: null, cancellationToken); - // Autogenerated.SubtleVerifyResponse response; - - // try - // { - // response = await client.SubtleVerifyAlpha1Async(envelope, options); - // } - // catch (RpcException ex) - // { - // throw new DaprException( - // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", - // ex); - // } - - // return response.Valid; - //} - - #endregion - - - #endregion - - #region Distributed Lock API - /// - [Obsolete] - public async override Task Lock( - string storeName, - string resourceId, - string lockOwner, - Int32 expiryInSeconds, - CancellationToken cancellationToken = default) - { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - - if (expiryInSeconds == 0 || expiryInSeconds < 0) - { - throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); - } + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + } - var request = new Autogenerated.TryLockRequest() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - ExpiryInSeconds = expiryInSeconds - }; + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} + + #endregion + + + #endregion + + #region Distributed Lock API + /// + [Obsolete] + public async override Task Lock( + string storeName, + string resourceId, + string lockOwner, + Int32 expiryInSeconds, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); - try - { - var options = CreateCallOptions(headers: null, cancellationToken); - var response = await client.TryLockAlpha1Async(request, options); - return new TryLockResponse() - { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - Success = response.Success - }; - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + if (expiryInSeconds == 0 || expiryInSeconds < 0) + { + throw new ArgumentException("The value cannot be zero or less than zero: " + expiryInSeconds); } - /// - [Obsolete] - public async override Task Unlock( - string storeName, - string resourceId, - string lockOwner, - CancellationToken cancellationToken = default) + var request = new Autogenerated.TryLockRequest() { - ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); - ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner, + ExpiryInSeconds = expiryInSeconds + }; - var request = new Autogenerated.UnlockRequest() + try + { + var options = CreateCallOptions(headers: null, cancellationToken); + var response = await client.TryLockAlpha1Async(request, options); + return new TryLockResponse() { StoreName = storeName, ResourceId = resourceId, - LockOwner = lockOwner + LockOwner = lockOwner, + Success = response.Success }; + } + catch (RpcException ex) + { + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - var options = CreateCallOptions(headers: null, cancellationToken); - Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); - try - { - response = await client.UnlockAlpha1Async(request, options); - } - catch (RpcException ex) - { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } + /// + [Obsolete] + public async override Task Unlock( + string storeName, + string resourceId, + string lockOwner, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(resourceId, nameof(resourceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(lockOwner, nameof(lockOwner)); + + var request = new Autogenerated.UnlockRequest() + { + StoreName = storeName, + ResourceId = resourceId, + LockOwner = lockOwner + }; - return new UnlockResponse(GetUnLockStatus(response.Status)); + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.UnlockResponse response = new Autogenerated.UnlockResponse(); + try + { + response = await client.UnlockAlpha1Async(request, options); + } + catch (RpcException ex) + { + throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } - #endregion + return new UnlockResponse(GetUnLockStatus(response.Status)); + } - #region Dapr Sidecar Methods + #endregion - /// - public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) - { - var path = "/v1.0/healthz"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + #region Dapr Sidecar Methods - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + try + { + using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) { - var path = "/v1.0/healthz/outbound"; - var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + return false; + } + } - if (this.apiTokenHeader is not null) - { - request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + /// + public override async Task CheckOutboundHealthAsync(CancellationToken cancellationToken = default) + { + var path = "/v1.0/healthz/outbound"; + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); - try - { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - return response.IsSuccessStatusCode; - } - catch (HttpRequestException) - { - return false; - } + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - /// - public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + try { - while (true) - { - var response = await CheckOutboundHealthAsync(cancellationToken); - if (response) - { - break; - } - await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); - } + using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + return response.IsSuccessStatusCode; } - - /// - public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + catch (HttpRequestException) { - await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + return false; } + } - /// - public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + /// + public override async Task WaitForSidecarAsync(CancellationToken cancellationToken = default) + { + while (true) { - var options = CreateCallOptions(headers: null, cancellationToken); - try - { - var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); - return new DaprMetadata(response.Id ?? "", - response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? - new List(), - response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? - new Dictionary(), - response.RegisteredComponents?.Select(c => - new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? - new List()); - } - catch (RpcException ex) + var response = await CheckOutboundHealthAsync(cancellationToken); + if (response) { - throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + break; } + await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); } + } + + /// + public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) + { + await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); + } - /// - public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + /// + public override async Task GetMetadataAsync(CancellationToken cancellationToken = default) + { + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); + return new DaprMetadata(response.Id ?? "", + response.ActorRuntime?.ActiveActors?.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList() ?? + new List(), + response.ExtendedMetadata?.ToDictionary(c => c.Key, c => c.Value) ?? + new Dictionary(), + response.RegisteredComponents?.Select(c => + new DaprComponentsMetadata(c.Name, c.Type, c.Version, c.Capabilities.ToArray())).ToList() ?? + new List()); + } + catch (RpcException ex) { - ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); + throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } - var envelope = new Autogenerated.SetMetadataRequest() - { - Key = attributeName, - Value = attributeValue - }; + /// + public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); - var options = CreateCallOptions(headers: null, cancellationToken); + var envelope = new Autogenerated.SetMetadataRequest() + { + Key = attributeName, + Value = attributeValue + }; - try - { - _ = await this.Client.SetMetadataAsync(envelope, options); - } - catch (RpcException ex) - { - throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); - } - } - #endregion + var options = CreateCallOptions(headers: null, cancellationToken); - protected override void Dispose(bool disposing) + try { - if (disposing) - { - this.channel.Dispose(); - this.httpClient.Dispose(); - } + _ = await this.Client.SetMetadataAsync(envelope, options); } + catch (RpcException ex) + { + throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + #endregion - #region Helper Methods - - private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + protected override void Dispose(bool disposing) + { + if (disposing) { - var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + this.channel.Dispose(); + this.httpClient.Dispose(); + } + } - options.Headers.Add("User-Agent", UserAgent().ToString()); + #region Helper Methods - // add token for dapr api token based authentication - if (this.apiTokenHeader is not null) - { - options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); - } + private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancellationToken) + { + var options = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - return options; - } + options.Headers.Add("User-Agent", UserAgent().ToString()); - /// - /// Makes Grpc call using the cancellationToken and handles Errors. - /// All common exception handling logic will reside here. - /// - /// - /// - /// - /// - private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + // add token for dapr api token based authentication + if (this.apiTokenHeader is not null) { - var callOptions = CreateCallOptions(headers: null, cancellationToken); - return await callFunc.Invoke(callOptions); + options.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); } - private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) - { - var stateRequestOptions = new Autogenerated.StateOptions(); + return options; + } - if (stateOptions.Consistency != null) - { - stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); - } + /// + /// Makes Grpc call using the cancellationToken and handles Errors. + /// All common exception handling logic will reside here. + /// + /// + /// + /// + /// + private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + { + var callOptions = CreateCallOptions(headers: null, cancellationToken); + return await callFunc.Invoke(callOptions); + } - if (stateOptions.Concurrency != null) - { - stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); - } + private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stateOptions) + { + var stateRequestOptions = new Autogenerated.StateOptions(); - return stateRequestOptions; + if (stateOptions.Consistency != null) + { + stateRequestOptions.Consistency = GetStateConsistencyForConsistencyMode(stateOptions.Consistency.Value); } - private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + if (stateOptions.Concurrency != null) { - return consistencyMode switch - { - ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, - ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, - _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") - }; + stateRequestOptions.Concurrency = GetStateConcurrencyForConcurrencyMode(stateOptions.Concurrency.Value); } - private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + return stateRequestOptions; + } + + private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + { + return consistencyMode switch { - return concurrencyMode switch - { - ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, - ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, - _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") - }; - } + ConsistencyMode.Eventual => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyEventual, + ConsistencyMode.Strong => Autogenerated.StateOptions.Types.StateConsistency.ConsistencyStrong, + _ => throw new ArgumentException($"{consistencyMode} Consistency Mode is not supported.") + }; + } - private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + { + return concurrencyMode switch { - return status switch - { - Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, - Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, - Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, - Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, - _ => throw new ArgumentException($"{status} Status is not supported.") - }; - } + ConcurrencyMode.FirstWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyFirstWrite, + ConcurrencyMode.LastWrite => Autogenerated.StateOptions.Types.StateConcurrency.ConcurrencyLastWrite, + _ => throw new ArgumentException($"{concurrencyMode} Concurrency Mode is not supported.") + }; + } - #endregion Helper Methods + private static LockStatus GetUnLockStatus(Autogenerated.UnlockResponse.Types.Status status) + { + return status switch + { + Autogenerated.UnlockResponse.Types.Status.Success => LockStatus.Success, + Autogenerated.UnlockResponse.Types.Status.LockDoesNotExist => LockStatus.LockDoesNotExist, + Autogenerated.UnlockResponse.Types.Status.LockBelongsToOthers => LockStatus.LockBelongsToOthers, + Autogenerated.UnlockResponse.Types.Status.InternalError => LockStatus.InternalError, + _ => throw new ArgumentException($"{status} Status is not supported.") + }; } + + #endregion Helper Methods } diff --git a/src/Dapr.Client/Extensions/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs deleted file mode 100644 index df9c9ad33..000000000 --- a/src/Dapr.Client/Extensions/EnumExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2023 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -#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.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 5044876a9..3037485a9 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -16,9 +16,11 @@ [assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -27,6 +29,7 @@ [assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AI.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -40,3 +43,4 @@ [assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/DaprClientUtilities.cs b/src/Dapr.Common/DaprClientUtilities.cs new file mode 100644 index 000000000..1aa860bbe --- /dev/null +++ b/src/Dapr.Common/DaprClientUtilities.cs @@ -0,0 +1,65 @@ +using System.Net.Http.Headers; +using System.Reflection; +using Grpc.Core; + +namespace Dapr.Common; + +internal static class DaprClientUtilities +{ + /// + /// Provisions the gRPC call options used to provision the various Dapr clients. + /// + /// The Dapr API token, if any. + /// The assembly the user agent is built from. + /// Cancellation token. + /// The gRPC call options. + internal static CallOptions ConfigureGrpcCallOptions(Assembly assembly, string? daprApiToken, CancellationToken cancellationToken = default) + { + var callOptions = new CallOptions(headers: new Metadata(), cancellationToken: cancellationToken); + + //Add the user-agent header to the gRPC call options + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + var userAgent = new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}").ToString(); + callOptions.Headers!.Add("User-Agent", userAgent); + + //Add the API token to the headers as well if it's populated + if (daprApiToken is not null) + { + var apiTokenHeader = GetDaprApiTokenHeader(daprApiToken); + if (apiTokenHeader is not null) + { + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } + } + + return callOptions; + } + + /// + /// Used to create the user-agent from the assembly attributes. + /// + /// The assembly the client is being built for. + /// The header value containing the user agent information. + public static ProductInfoHeaderValue GetUserAgent(Assembly assembly) + { + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } + + /// + /// Used to provision the header used for the Dapr API token on the HTTP or gRPC connection. + /// + /// The value of the Dapr API token. + /// If a Dapr API token exists, the key/value pair to use for the header; otherwise null. + public static KeyValuePair? GetDaprApiTokenHeader(string? daprApiToken) => + string.IsNullOrWhiteSpace(daprApiToken) + ? null + : new KeyValuePair("dapr-api-token", daprApiToken); +} + diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 254953241..60a9827a2 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -1,4 +1,18 @@ -using System.Text.Json; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Reflection; +using System.Text.Json; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; @@ -170,8 +184,9 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) /// Builds out the inner DaprClient that provides the core shape of the /// runtime gRPC client used by the consuming package. ///
+ /// The assembly the dependencies are being built for. /// - protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint, string daprApiToken) BuildDaprClientDependencies(Assembly assembly) { var grpcEndpoint = new Uri(this.GrpcEndpoint); if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") @@ -184,22 +199,48 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - + var httpEndpoint = new Uri(this.HttpEndpoint); if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { throw new InvalidOperationException("The HTTP endpoint must use http or https."); } - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + //Configure the HTTP client + var httpClient = ConfigureHttpClient(assembly); + this.GrpcChannelOptions.HttpClient = httpClient; + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + return (channel, httpClient, httpEndpoint, this.DaprApiToken); + } + /// + /// Configures the HTTP client. + /// + /// The assembly the user agent is built from. + /// The HTTP client to interact with the Dapr runtime with. + private HttpClient ConfigureHttpClient(Assembly assembly) + { var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + + //Set the timeout as necessary if (this.Timeout > TimeSpan.Zero) { httpClient.Timeout = this.Timeout; } + + //Set the user agent + var userAgent = DaprClientUtilities.GetUserAgent(assembly); + httpClient.DefaultRequestHeaders.Add("User-Agent", userAgent.ToString()); + + //Set the API token + var apiTokenHeader = DaprClientUtilities.GetDaprApiTokenHeader(this.DaprApiToken); + if (apiTokenHeader is not null) + { + httpClient.DefaultRequestHeaders.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + } - return (channel, httpClient, httpEndpoint); + return httpClient; } /// diff --git a/src/Dapr.Common/Extensions/EnumExtensions.cs b/src/Dapr.Common/Extensions/EnumExtensions.cs index ff9b43706..0216c9258 100644 --- a/src/Dapr.Common/Extensions/EnumExtensions.cs +++ b/src/Dapr.Common/Extensions/EnumExtensions.cs @@ -1,4 +1,17 @@ -using System.Reflection; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Reflection; using System.Runtime.Serialization; namespace Dapr.Common.Extensions; diff --git a/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs new file mode 100644 index 000000000..be79c101a --- /dev/null +++ b/src/Dapr.Common/JsonConverters/GenericEnumJsonConverter.cs @@ -0,0 +1,70 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.Extensions; + +namespace Dapr.Common.JsonConverters; + +/// +/// A JsonConverter used to convert from an enum to a string and vice versa, but using the Enum extension written to pull +/// the value from the [EnumMember] attribute, if present. +/// +/// The enum type to convert. +internal sealed class GenericEnumJsonConverter : JsonConverter where T : struct, Enum +{ + private static readonly Dictionary enumMemberCache = new(); + + static GenericEnumJsonConverter() + { + foreach (var enumValue in Enum.GetValues()) + { + var enumMemberValue = enumValue.GetValueFromEnumMember(); + enumMemberCache[enumMemberValue] = enumValue; + } + } + + /// Reads and converts the JSON to type . + /// The reader. + /// The type to convert. + /// An object that specifies serialization options to use. + /// The converted value. + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + //Get the string value from the JSON reader + var value = reader.GetString(); + + //Try pulling the value from the cache + if (value is not null && enumMemberCache.TryGetValue(value, out var enumValue)) + { + return enumValue; + } + + //If no match found, throw an exception + throw new JsonException($"Invalid valid for {typeToConvert.Name}: {value}"); + } + + /// Writes a specified value as JSON. + /// The writer to write to. + /// The value to convert to JSON. + /// An object that specifies serialization options to use. + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + //Get the value from the EnumMember attribute, if any + var enumMemberValue = value.GetValueFromEnumMember(); + + //Write the value to the JSON writer + writer.WriteStringValue(enumMemberValue); + } +} diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs index 390d52236..509486a1e 100644 --- a/src/Dapr.Jobs/DaprJobsClientBuilder.cs +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -12,6 +12,7 @@ // ------------------------------------------------------------------------ using Dapr.Common; +using Microsoft.Extensions.Configuration; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; namespace Dapr.Jobs; @@ -21,17 +22,22 @@ namespace Dapr.Jobs; /// public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder { + /// + /// Used to initialize a new instance of . + /// + /// An optional instance of . + public DaprJobsClientBuilder(IConfiguration? configuration = null) : base(configuration) + { + } + /// /// Builds the client instance from the properties of the builder. /// /// The Dapr client instance. public override DaprJobsClient Build() { - var daprClientDependencies = this.BuildDaprClientDependencies(); - + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprJobsClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; - - return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader); + return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index f23ef67fd..1f035220e 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -11,8 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System.Net.Http.Headers; -using System.Reflection; +using Dapr.Common; using Dapr.Jobs.Models; using Dapr.Jobs.Models.Responses; using Google.Protobuf; @@ -28,29 +27,35 @@ namespace Dapr.Jobs; internal sealed class DaprJobsGrpcClient : DaprJobsClient { /// - /// Present only for testing purposes. + /// The HTTP client used by the client for calling the Dapr runtime. /// - internal readonly HttpClient httpClient; - + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; /// - /// Used to populate options headers with API token value. + /// The Dapr API token value. /// - internal readonly KeyValuePair? apiTokenHeader; - - private readonly Autogenerated.Dapr.DaprClient client; - private readonly string userAgent = UserAgent().ToString(); - - // property exposed for testing purposes - internal Autogenerated.Dapr.DaprClient Client => client; + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + internal Autogenerated.Dapr.DaprClient Client { get; } internal DaprJobsGrpcClient( Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, - KeyValuePair? apiTokenHeader) + string? daprApiToken) { - this.client = innerClient; - this.httpClient = httpClient; - this.apiTokenHeader = apiTokenHeader; + this.Client = innerClient; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -107,11 +112,11 @@ public override async Task ScheduleJobAsync(string jobName, DaprJobSchedule sche var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); try { - await client.ScheduleJobAlpha1Async(envelope, callOptions); + await Client.ScheduleJobAlpha1Async(envelope, grpcCallOptions).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -146,8 +151,8 @@ public override async Task GetJobAsync(string jobName, Cancellat try { var envelope = new Autogenerated.GetJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - var response = await client.GetJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + var response = await Client.GetJobAlpha1Async(envelope, grpcCallOptions); return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule)) { DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, @@ -190,8 +195,8 @@ public override async Task DeleteJobAsync(string jobName, CancellationToken canc try { var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; - var callOptions = CreateCallOptions(headers: null, cancellationToken); - await client.DeleteJobAlpha1Async(envelope, callOptions); + var grpcCallOptions = DaprClientUtilities.ConfigureGrpcCallOptions(typeof(DaprJobsClient).Assembly, this.DaprApiToken, cancellationToken); + await Client.DeleteJobAlpha1Async(envelope, grpcCallOptions); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -213,36 +218,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - this.httpClient.Dispose(); + this.HttpClient.Dispose(); } } - - private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) - { - var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); - - callOptions.Headers!.Add("User-Agent", this.userAgent); - - if (apiTokenHeader is not null) - { - callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); - } - - return callOptions; - } - - /// - /// Returns the value for the User-Agent. - /// - /// A containing the value to use for the User-Agent. - private static ProductInfoHeaderValue UserAgent() - { - var assembly = typeof(DaprJobsClient).Assembly; - var assemblyVersion = assembly - .GetCustomAttributes() - .FirstOrDefault()? - .InformationalVersion; - - return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); - } } diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index 93265837b..e3680fd83 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -25,27 +26,29 @@ public static class DaprJobsServiceCollectionExtensions /// Adds Dapr Jobs client support to the service collection. /// /// The . - /// Optionally allows greater configuration of the . + /// Optionally allows greater configuration of the using injected services. /// The lifetime of the registered services. - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); //Register the IHttpClientFactory implementation serviceCollection.AddHttpClient(); - + var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); - var builder = new DaprJobsClientBuilder(); + var builder = new DaprJobsClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); - configure?.Invoke(builder); + configure?.Invoke(serviceProvider, builder); return builder.Build(); }); - + switch (lifetime) { case ServiceLifetime.Scoped: @@ -59,35 +62,6 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi serviceCollection.TryAddSingleton(registration); break; } - - return serviceCollection; - } - - /// - /// Adds Dapr Jobs client support to the service collection. - /// - /// The . - /// Optionally allows greater configuration of the using injected services. - /// The lifetime of the registered services. - /// - public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) - { - ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); - - //Register the IHttpClientFactory implementation - serviceCollection.AddHttpClient(); - - serviceCollection.TryAddSingleton(serviceProvider => - { - var httpClientFactory = serviceProvider.GetRequiredService(); - - var builder = new DaprJobsClientBuilder(); - builder.UseHttpClientFactory(httpClientFactory); - - configure?.Invoke(serviceProvider, builder); - - return builder.Build(); - }); return serviceCollection; } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs index b94bc5cdf..829bab75d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeClientBuilder.cs @@ -39,9 +39,8 @@ public DaprPublishSubscribeClientBuilder(IConfiguration? configuration = null) : ///
public override DaprPublishSubscribeClient Build() { - var daprClientDependencies = BuildDaprClientDependencies(); + var daprClientDependencies = BuildDaprClientDependencies(typeof(DaprPublishSubscribeClient).Assembly); var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); - - return new DaprPublishSubscribeGrpcClient(client); + return new DaprPublishSubscribeGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); } } diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index df6ccdcfe..39024cb35 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -20,14 +20,36 @@ namespace Dapr.Messaging.PublishSubscribe; ///
internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClient { - private readonly P.DaprClient daprClient; + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient; + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken; + /// + /// The autogenerated Dapr client. + /// + /// + /// Property exposed for testing purposes. + /// + private readonly P.DaprClient Client; /// /// Creates a new instance of a /// - public DaprPublishSubscribeGrpcClient(P.DaprClient client) + public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) { - daprClient = client; + Client = client; + this.HttpClient = httpClient; + this.DaprApiToken = daprApiToken; } /// @@ -41,7 +63,7 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client) /// public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) { - var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, Client); await receiver.SubscribeAsync(cancellationToken); return receiver; } diff --git a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs index fe9b7c417..3d9e3ee8d 100644 --- a/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs +++ b/src/Dapr.Messaging/PublishSubscribe/Extensions/PublishSubscribeServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Dapr.Messaging.PublishSubscribe.Extensions; @@ -25,8 +26,9 @@ public static IServiceCollection AddDaprPubSubClient(this IServiceCollection ser var registration = new Func(serviceProvider => { var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); - var builder = new DaprPublishSubscribeClientBuilder(); + var builder = new DaprPublishSubscribeClientBuilder(configuration); builder.UseHttpClientFactory(httpClientFactory); configure?.Invoke(serviceProvider, builder); diff --git a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto index 0eb882b89..fc5e99835 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/common/v1/common.proto @@ -157,4 +157,4 @@ message ConfigurationItem { // the metadata which will be passed to/from configuration store component. map metadata = 3; -} \ No newline at end of file +} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto index 51dee5539..144e8c87a 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -340,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} \ No newline at end of file +message HealthCheckResponse {} diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index ecf0f76f7..470a0d009 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -202,6 +202,9 @@ service Dapr { // Delete a job rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} + + // Converse with a LLM service + rpc ConverseAlpha1(ConversationRequest) returns (ConversationResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -1206,7 +1209,7 @@ message Job { // // Systemd timer style cron accepts 6 fields: // seconds | minutes | hours | day of month | month | day of week - // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat // // "0 30 * * * *" - every hour on the half hour // "0 15 3 * * *" - every day at 03:15 @@ -1274,4 +1277,56 @@ message DeleteJobRequest { // DeleteJobResponse is the message response to delete the job by name. message DeleteJobResponse { // Empty +} + +// ConversationRequest is the request object for Conversation. +message ConversationRequest { + // The name of Conversation component + string name = 1; + + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 2; + + // Inputs for the conversation, support multiple input in one time. + repeated ConversationInput inputs = 3; + + // Parameters for all custom fields. + map parameters = 4; + + // The metadata passing to conversation components. + map metadata = 5; + + // Scrub PII data that comes back from the LLM + optional bool scrubPII = 6; + + // Temperature for the LLM to optimize for creativity or predictability + optional double temperature = 7; +} + +message ConversationInput { + // The message to send to the llm + string message = 1; + + // The role to set for the message + optional string role = 2; + + // Scrub PII data that goes into the LLM + optional bool scrubPII = 3; +} + +// ConversationResult is the result for one input. +message ConversationResult { + // Result for the one conversation input. + string result = 1; + // Parameters for all custom fields. + map parameters = 2; +} + +// ConversationResponse is the response for Conversation. +message ConversationResponse { + // The ID of an existing chat (like in ChatGPT) + optional string contextID = 1; + + // An array of results. + repeated ConversationResult outputs = 2; } \ No newline at end of file diff --git a/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs new file mode 100644 index 000000000..901c4b656 --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/DaprConversationClientBuilderTest.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.AI.Conversation; + +namespace Dapr.AI.Test.Conversation; + +public class DaprConversationClientBuilderTest +{ + [Fact] + public void Build_WithDefaultConfiguration_ShouldReturnNewInstanceOfDaprConversationClient() + { + // Arrange + var conversationClientBuilder = new DaprConversationClientBuilder(); + + // Act + var client = conversationClientBuilder.Build(); + + // Assert + Assert.NotNull(client); + Assert.IsType(client); + } +} diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs new file mode 100644 index 000000000..95a8e1e8c --- /dev/null +++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Dapr.AI.Conversation; +using Dapr.AI.Conversation.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.AI.Test.Conversation.Extensions; + +public class DaprAiConversationBuilderExtensionsTest +{ + [Fact] + public void AddDaprConversationClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprAiConversation(); + + var app = services.BuildServiceProvider(); + + var conversationClient = app.GetRequiredService() as DaprConversationClient; + + Assert.NotNull(conversationClient!.DaprApiToken); + Assert.Equal(apiToken, conversationClient.DaprApiToken); + } + + [Fact] + public void AddDaprAiConversation_WithoutConfigure_ShouldAddServices() + { + var services = new ServiceCollection(); + var builder = services.AddDaprAiConversation(); + Assert.NotNull(builder); + } + + [Fact] + public void AddDaprAiConversation_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + services.AddDaprAiConversation(); + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprConversationClient = serviceProvider.GetService(); + Assert.NotNull(daprConversationClient); + } + + [Fact] + public void AddDaprAiConversation_NullServices_ShouldThrowException() + { + IServiceCollection services = null; + Assert.Throws(() => services.AddDaprAiConversation()); + } +} diff --git a/test/Dapr.AI.Test/Dapr.AI.Test.csproj b/test/Dapr.AI.Test/Dapr.AI.Test.csproj new file mode 100644 index 000000000..f937f64e2 --- /dev/null +++ b/test/Dapr.AI.Test/Dapr.AI.Test.csproj @@ -0,0 +1,28 @@ + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs deleted file mode 100644 index 83c4354f9..000000000 --- a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs +++ /dev/null @@ -1,38 +0,0 @@ -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.Common.Test/Extensions/EnumExtensionsTest.cs b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs index 84e2998d6..e7b2d014b 100644 --- a/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs +++ b/test/Dapr.Common.Test/Extensions/EnumExtensionsTest.cs @@ -12,14 +12,14 @@ public void GetValueFromEnumMember_RedResolvesAsExpected() var value = TestEnum.Red.GetValueFromEnumMember(); Assert.Equal("red", value); } - + [Fact] public void GetValueFromEnumMember_YellowResolvesAsExpected() { var value = TestEnum.Yellow.GetValueFromEnumMember(); Assert.Equal("YELLOW", value); } - + [Fact] public void GetValueFromEnumMember_BlueResolvesAsExpected() { @@ -27,6 +27,7 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected() Assert.Equal("Blue", value); } } + public enum TestEnum { [EnumMember(Value = "red")] @@ -35,4 +36,3 @@ public enum TestEnum Yellow, Blue } - diff --git a/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs new file mode 100644 index 000000000..065a74220 --- /dev/null +++ b/test/Dapr.Common.Test/JsonConverters/GenericEnumJsonConverterTest.cs @@ -0,0 +1,52 @@ +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dapr.Common.JsonConverters; +using Xunit; + +namespace Dapr.Common.Test.JsonConverters; + +public class GenericEnumJsonConverterTest +{ + [Fact] + public void ShouldSerializeWithEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Red); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}", serializedValue); + } + + [Fact] + public void ShouldSerializeWithoutEnumMemberAttribute() + { + var testValue = new TestType("ColorTest", Color.Green); + var serializedValue = JsonSerializer.Serialize(testValue); + Assert.Equal("{\"Name\":\"ColorTest\",\"Color\":\"Green\"}", serializedValue); + } + + [Fact] + public void ShouldDeserializeWithEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"definitely-not-red\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Red, deserializedValue.Color); + } + + [Fact] + public void ShouldDeserializeWithoutEnumMemberAttribute() + { + const string json = "{\"Name\":\"ColorTest\",\"Color\":\"Green\"}"; + var deserializedValue = JsonSerializer.Deserialize(json); + Assert.Equal("ColorTest", deserializedValue.Name); + Assert.Equal(Color.Green, deserializedValue.Color); + } + + private record TestType(string Name, Color Color); + + [JsonConverter(typeof(GenericEnumJsonConverter))] + private enum Color { + [EnumMember(Value="definitely-not-red")] + Red, + Green }; +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 281477d4e..bd5e4acd0 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -12,31 +12,57 @@ // ------------------------------------------------------------------------ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Dapr.Jobs.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Dapr.Jobs.Test.Extensions; public class DaprJobsServiceCollectionExtensionsTest { + [Fact] + public void AddDaprJobsClient_FromIConfiguration() + { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprJobsClient(); + + var app = services.BuildServiceProvider(); + + var jobsClient = app.GetRequiredService() as DaprJobsGrpcClient; + + Assert.NotNull(jobsClient!.DaprApiToken); + Assert.Equal(apiToken, jobsClient.DaprApiToken); + } + [Fact] public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var clientBuilder = new Action(builder => - builder.UseDaprApiToken("abc")); + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); services.AddDaprJobsClient(); //Sets a default API token value of an empty string services.AddDaprJobsClient(clientBuilder); //Sets the API token value var serviceProvider = services.BuildServiceProvider(); var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; - - Assert.Null(daprJobClient!.apiTokenHeader); - Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + + Assert.NotNull(daprJobClient!.HttpClient); + Assert.False(daprJobClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); } [Fact] @@ -63,8 +89,8 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() services.AddDaprJobsClient((provider, builder) => { var configProvider = provider.GetRequiredService(); - var daprApiToken = configProvider.GetApiTokenValue(); - builder.UseDaprApiToken(daprApiToken); + var apiToken = TestSecretRetriever.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); }); var serviceProvider = services.BuildServiceProvider(); @@ -72,10 +98,15 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient Assert.NotNull(client); - Assert.NotNull(client.apiTokenHeader); - Assert.True(client.apiTokenHeader.HasValue); - Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); - Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); } [Fact] @@ -83,7 +114,7 @@ public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Singleton); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Singleton); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -100,7 +131,7 @@ public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Scoped); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Scoped); var serviceProvider = services.BuildServiceProvider(); await using var scope1 = serviceProvider.CreateAsyncScope(); @@ -119,7 +150,7 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() { var services = new ServiceCollection(); - services.AddDaprJobsClient(options => { }, ServiceLifetime.Transient); + services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Transient); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -132,6 +163,6 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() private class TestSecretRetriever { - public string GetApiTokenValue() => "abcdef"; + public static string GetApiTokenValue() => "abcdef"; } } From 2fe08c9434f0e2e3271f9df91fe3004471742dd3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 10 Dec 2024 22:17:00 -0600 Subject: [PATCH 49/69] Updated protos to latest in dapr/dapr (#1420) Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- .../Protos/dapr/proto/runtime/v1/dapr.proto | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index 470a0d009..0ab371e6d 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -151,25 +151,39 @@ service Dapr { rpc SubtleVerifyAlpha1(SubtleVerifyRequest) returns (SubtleVerifyResponse); // Starts a new instance of a workflow - rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) { + option deprecated = true; + } // Gets details about a started workflow instance - rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) { + option deprecated = true; + } // Purge Workflow - rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Terminates a running workflow instance - rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Pauses a running workflow instance - rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Resumes a paused workflow instance - rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Raise an event to a running workflow instance - rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) { + option deprecated = true; + } // Starts a new instance of a workflow rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} @@ -191,6 +205,7 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Shutdown the sidecar rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} From 1e77e3992d12a7ecaaafb9c5bcdbfbd39d0b81e8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 11 Dec 2024 13:42:23 -0600 Subject: [PATCH 50/69] Conversation builder consistency changes (#1423) * Corrected several unit tests Signed-off-by: Whit Waldo * Updated extension name for consistency Signed-off-by: Whit Waldo * Updated registration name for consistency Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- examples/AI/ConversationalAI/Program.cs | 2 +- .../DaprAiConversationBuilderExtensions.cs | 2 +- ...DaprAiConversationBuilderExtensionsTest.cs | 121 +++++++++++++++++- ...aprJobsServiceCollectionExtensionsTests.cs | 10 +- 4 files changed, 121 insertions(+), 14 deletions(-) diff --git a/examples/AI/ConversationalAI/Program.cs b/examples/AI/ConversationalAI/Program.cs index bd3dc906a..6315db87a 100644 --- a/examples/AI/ConversationalAI/Program.cs +++ b/examples/AI/ConversationalAI/Program.cs @@ -3,7 +3,7 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddDaprAiConversation(); +builder.Services.AddDaprConversationClient(); var app = builder.Build(); diff --git a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs index 902fd82a3..2f049a906 100644 --- a/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs +++ b/src/Dapr.AI/Conversation/Extensions/DaprAiConversationBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class DaprAiConversationBuilderExtensions /// Registers the necessary functionality for the Dapr AI conversation functionality. /// /// - public static IDaprAiConversationBuilder AddDaprAiConversation(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + public static IDaprAiConversationBuilder AddDaprConversationClient(this IServiceCollection services, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) { ArgumentNullException.ThrowIfNull(services, nameof(services)); diff --git a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs index 95a8e1e8c..2ee321895 100644 --- a/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs +++ b/test/Dapr.AI.Test/Conversation/Extensions/DaprAiConversationBuilderExtensionsTest.cs @@ -13,7 +13,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; +using System.Threading.Tasks; using Dapr.AI.Conversation; using Dapr.AI.Conversation.Extensions; using Microsoft.Extensions.Configuration; @@ -34,7 +36,7 @@ public void AddDaprConversationClient_FromIConfiguration() var services = new ServiceCollection(); services.AddSingleton(configuration); - services.AddDaprAiConversation(); + services.AddDaprConversationClient(); var app = services.BuildServiceProvider(); @@ -45,18 +47,66 @@ public void AddDaprConversationClient_FromIConfiguration() } [Fact] - public void AddDaprAiConversation_WithoutConfigure_ShouldAddServices() + public void AddDaprConversationClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); - var builder = services.AddDaprAiConversation(); + + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); + + services.AddDaprConversationClient(); //Sets a default API token value of an empty string + services.AddDaprConversationClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + var daprConversationClient = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient!.HttpClient); + Assert.False(daprConversationClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprConversationClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprConversationClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var apiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService(); + + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); + } + + [Fact] + public void AddDaprConversationClient_WithoutConfigure_ShouldAddServices() + { + var services = new ServiceCollection(); + var builder = services.AddDaprConversationClient(); Assert.NotNull(builder); } [Fact] - public void AddDaprAiConversation_RegistersIHttpClientFactory() + public void AddDaprConversationClient_RegistersIHttpClientFactory() { var services = new ServiceCollection(); - services.AddDaprAiConversation(); + services.AddDaprConversationClient(); var serviceProvider = services.BuildServiceProvider(); var httpClientFactory = serviceProvider.GetService(); @@ -67,9 +117,66 @@ public void AddDaprAiConversation_RegistersIHttpClientFactory() } [Fact] - public void AddDaprAiConversation_NullServices_ShouldThrowException() + public void AddDaprConversationClient_NullServices_ShouldThrowException() { IServiceCollection services = null; - Assert.Throws(() => services.AddDaprAiConversation()); + Assert.Throws(() => services.AddDaprConversationClient()); + } + + [Fact] + public void AddDaprConversationClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var daprConversationClient1 = serviceProvider.GetService(); + var daprConversationClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + + Assert.Same(daprConversationClient1, daprConversationClient2); + } + + [Fact] + public async Task AddDaprConversationClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var daprConversationClient1 = scope1.ServiceProvider.GetService(); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var daprConversationClient2 = scope2.ServiceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + Assert.NotSame(daprConversationClient1, daprConversationClient2); + } + + [Fact] + public void AddDaprConversationClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprConversationClient((_, _) => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var daprConversationClient1 = serviceProvider.GetService(); + var daprConversationClient2 = serviceProvider.GetService(); + + Assert.NotNull(daprConversationClient1); + Assert.NotNull(daprConversationClient2); + Assert.NotSame(daprConversationClient1, daprConversationClient2); + } + + private class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; } } diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index bd5e4acd0..3b2c5f990 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -89,7 +89,7 @@ public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() services.AddDaprJobsClient((provider, builder) => { var configProvider = provider.GetRequiredService(); - var apiToken = TestSecretRetriever.GetApiTokenValue(); + var apiToken = configProvider.GetApiTokenValue(); builder.UseDaprApiToken(apiToken); }); @@ -114,7 +114,7 @@ public void RegisterJobsClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() { var services = new ServiceCollection(); - services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Singleton); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Singleton); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -131,7 +131,7 @@ public async Task RegisterJobsClient_ShouldRegisterScoped_WhenLifetimeIsScoped() { var services = new ServiceCollection(); - services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Scoped); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Scoped); var serviceProvider = services.BuildServiceProvider(); await using var scope1 = serviceProvider.CreateAsyncScope(); @@ -150,7 +150,7 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() { var services = new ServiceCollection(); - services.AddDaprJobsClient((serviceProvider, options) => { }, ServiceLifetime.Transient); + services.AddDaprJobsClient((_, _) => { }, ServiceLifetime.Transient); var serviceProvider = services.BuildServiceProvider(); var daprJobsClient1 = serviceProvider.GetService(); @@ -163,6 +163,6 @@ public void RegisterJobsClient_ShouldRegisterTransient_WhenLifetimeIsTransient() private class TestSecretRetriever { - public static string GetApiTokenValue() => "abcdef"; + public string GetApiTokenValue() => "abcdef"; } } From 9d36d3e62bad6511a11112c84c86aaa8f227bb56 Mon Sep 17 00:00:00 2001 From: Divya Perumal <38757978+divzi-p@users.noreply.github.com> Date: Thu, 12 Dec 2024 02:11:14 +0530 Subject: [PATCH 51/69] #906 -Added methods in status API supports for saving and reading binary data (#1116) * Added methods in status API supports for direct storage and reading of byte arrays #906 Signed-off-by: Divya Perumal Signed-off-by: Divya Perumal Signed-off-by: Siri Varma Vegiraju --- examples/Client/StateManagement/Program.cs | 5 +- .../StateStoreBinaryExample.cs | 47 ++ src/Dapr.Client/DaprClient.cs | 74 ++ src/Dapr.Client/DaprClientGrpc.cs | 645 ++++++++++++------ src/Dapr.Client/Extensions/EnumExtensions.cs | 41 ++ src/Dapr.Common/DaprGenericClientBuilder.cs | 4 +- src/Dapr.Jobs/DaprJobsGrpcClient.cs | 2 +- .../DaprJobsServiceCollectionExtensions.cs | 3 +- .../Protos/dapr/proto/runtime/v1/dapr.proto | 3 +- .../Extensions/EnumExtensionTest.cs | 38 ++ test/Dapr.Client.Test/StateApiTest.cs | 528 ++++++++++---- ...aprJobsServiceCollectionExtensionsTests.cs | 2 + 12 files changed, 1042 insertions(+), 350 deletions(-) create mode 100644 examples/Client/StateManagement/StateStoreBinaryExample.cs create mode 100644 src/Dapr.Client/Extensions/EnumExtensions.cs create mode 100644 test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs diff --git a/examples/Client/StateManagement/Program.cs b/examples/Client/StateManagement/Program.cs index 24e37d004..e9ef36979 100644 --- a/examples/Client/StateManagement/Program.cs +++ b/examples/Client/StateManagement/Program.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,8 @@ class Program new StateStoreExample(), new StateStoreTransactionsExample(), new StateStoreETagsExample(), - new BulkStateExample() + new BulkStateExample(), + new StateStoreBinaryExample() }; static async Task Main(string[] args) diff --git a/examples/Client/StateManagement/StateStoreBinaryExample.cs b/examples/Client/StateManagement/StateStoreBinaryExample.cs new file mode 100644 index 000000000..edf23704e --- /dev/null +++ b/examples/Client/StateManagement/StateStoreBinaryExample.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Dapr.Client; +using System.Threading.Tasks; +using System.Threading; +using Google.Protobuf; + +namespace Samples.Client +{ + public class StateStoreBinaryExample : Example + { + + private static readonly string stateKeyName = "binarydata"; + private static readonly string storeName = "statestore"; + + public override string DisplayName => "Using the State Store with binary data"; + + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + var state = "Test Binary Data"; + // convert variable in to byte array + var stateBytes = Encoding.UTF8.GetBytes(state); + await client.SaveByteStateAsync(storeName, stateKeyName, stateBytes.AsMemory(), cancellationToken: cancellationToken); + Console.WriteLine("Saved State!"); + + var responseBytes = await client.GetByteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + var savedState = Encoding.UTF8.GetString(ByteString.CopyFrom(responseBytes.Span).ToByteArray()); + + if (savedState == null) + { + Console.WriteLine("State not found in store"); + } + else + { + Console.WriteLine($"Got State: {savedState}"); + } + + await client.DeleteStateAsync(storeName, stateKeyName, cancellationToken: cancellationToken); + Console.WriteLine("Deleted State!"); + } + + + } +} diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 43c640a69..6be31a648 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -850,6 +850,80 @@ public abstract Task SaveStateAsync( IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + + /// + /// Saves the provided associated with the provided to the Dapr state + /// store + /// + /// The name of the state store. + /// The state key. + /// The binary data that will be stored in the state store. + /// Options for performing save state operation. + /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task SaveByteStateAsync( + string storeName, + string key, + ReadOnlyMemory binaryValue, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default); + + /// + ///Saves the provided associated with the provided using the + /// to the Dapr state. State store implementation will allow the update only if the attached ETag matches with the latest ETag in the state store. + /// + /// The name of the state store. + /// The state key. + /// The binary data that will be stored in the state store. + /// An ETag. + /// Options for performing save state operation. + /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used. + /// A that can be used to cancel the operation. + /// A that will complete when the operation has completed. + public abstract Task TrySaveByteStateAsync( + string storeName, + string key, + ReadOnlyMemory binaryValue, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default); + + + /// + /// Gets the current binary value associated with the from the Dapr state store. + /// + /// The name of state store to read from. + /// The state key. + /// The consistency mode . + /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. + public abstract Task> GetByteStateAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default); + + /// + /// Gets the current binary value associated with the from the Dapr state store and an ETag. + /// + /// The name of the state store. + /// The state key. + /// The consistency mode . + /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used. + /// A that can be used to cancel the operation. + /// A that will return the value when the operation has completed. This wraps the read value and an ETag. + public abstract Task<(ReadOnlyMemory, string etag)> GetByteStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default); + /// /// Tries to save the state associated with the provided using the /// to the Dapr state. State store implementation will allow the update only if the attached ETag matches with the latest ETag in the state store. diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index bd0bd1d01..40df4767c 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -74,6 +74,7 @@ internal DaprClientGrpc( } #region Publish Apis + /// public override Task PublishEventAsync( string pubsubName, @@ -86,7 +87,8 @@ public override Task PublishEventAsync( ArgumentVerifier.ThrowIfNull(data, nameof(data)); var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, null, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + return MakePublishRequest(pubsubName, topicName, content, null, + data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); } public override Task PublishEventAsync( @@ -102,7 +104,8 @@ public override Task PublishEventAsync( ArgumentVerifier.ThrowIfNull(metadata, nameof(metadata)); var content = TypeConverters.ToJsonByteString(data, this.JsonSerializerOptions); - return MakePublishRequest(pubsubName, topicName, content, metadata, data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); + return MakePublishRequest(pubsubName, topicName, content, metadata, + data is CloudEvent ? Constants.ContentTypeCloudEvent : null, cancellationToken); } /// @@ -138,7 +141,8 @@ public override Task PublishByteEventAsync( { ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); ArgumentVerifier.ThrowIfNullOrEmpty(topicName, nameof(topicName)); - return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, cancellationToken); + return MakePublishRequest(pubsubName, topicName, ByteString.CopyFrom(data.Span), metadata, dataContentType, + cancellationToken); } private async Task MakePublishRequest( @@ -149,11 +153,7 @@ private async Task MakePublishRequest( string dataContentType, CancellationToken cancellationToken) { - var envelope = new Autogenerated.PublishEventRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; + var envelope = new Autogenerated.PublishEventRequest() { PubsubName = pubsubName, Topic = topicName, }; if (content != null) { @@ -177,7 +177,8 @@ private async Task MakePublishRequest( } catch (RpcException ex) { - throw new DaprException("Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Publish operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } } @@ -194,7 +195,7 @@ public override Task> BulkPublishEventAsync( ArgumentVerifier.ThrowIfNull(events, nameof(events)); return MakeBulkPublishRequest(pubsubName, topicName, events, metadata, cancellationToken); } - + private async Task> MakeBulkPublishRequest( string pubsubName, string topicName, @@ -202,12 +203,8 @@ private async Task> MakeBulkPublishRequest( Dictionary metadata, CancellationToken cancellationToken) { - var envelope = new Autogenerated.BulkPublishRequest() - { - PubsubName = pubsubName, - Topic = topicName, - }; - + var envelope = new Autogenerated.BulkPublishRequest() { PubsubName = pubsubName, Topic = topicName, }; + Dictionary> entryMap = new Dictionary>(); for (int counter = 0; counter < events.Count; counter++) @@ -216,14 +213,17 @@ private async Task> MakeBulkPublishRequest( { EntryId = counter.ToString(), Event = TypeConverters.ToJsonByteString(events[counter], this.jsonSerializerOptions), - ContentType = events[counter] is CloudEvent ? Constants.ContentTypeCloudEvent : Constants.ContentTypeApplicationJson, - Metadata = {}, + ContentType = + events[counter] is CloudEvent + ? Constants.ContentTypeCloudEvent + : Constants.ContentTypeApplicationJson, + Metadata = { }, }; envelope.Entries.Add(entry); entryMap.Add(counter.ToString(), new BulkPublishEntry( entry.EntryId, events[counter], entry.ContentType, entry.Metadata)); } - + if (metadata != null) { foreach (var kvp in metadata) @@ -231,22 +231,23 @@ private async Task> MakeBulkPublishRequest( envelope.Metadata.Add(kvp.Key, kvp.Value); } } - + var options = CreateCallOptions(headers: null, cancellationToken); try { var response = await client.BulkPublishEventAlpha1Async(envelope, options); - List> failedEntries = new List>(); - + List> failedEntries = + new List>(); + foreach (var entry in response.FailedEntries) { BulkPublishResponseFailedEntry domainEntry = new BulkPublishResponseFailedEntry( entryMap[entry.EntryId], entry.Error); failedEntries.Add(domainEntry); } - + var bulkPublishResponse = new BulkPublishResponse(failedEntries); return bulkPublishResponse; @@ -257,6 +258,7 @@ private async Task> MakeBulkPublishRequest( "failure. See InnerException for details.", ex); } } + #endregion #region InvokeBinding Apis @@ -296,14 +298,18 @@ public override async Task InvokeBindingAsync( } catch (JsonException ex) { - throw new DaprException("Binding operation failed: the response payload could not be deserialized. See InnerException for details.", ex); + throw new DaprException( + "Binding operation failed: the response payload could not be deserialized. See InnerException for details.", + ex); } } - public override async Task InvokeBindingAsync(BindingRequest request, CancellationToken cancellationToken = default) + public override async Task InvokeBindingAsync(BindingRequest request, + CancellationToken cancellationToken = default) { var bytes = ByteString.CopyFrom(request.Data.Span); - var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, request.Metadata, cancellationToken); + var response = await this.MakeInvokeBindingRequestAsync(request.BindingName, request.Operation, bytes, + request.Metadata, cancellationToken); return new BindingResponse(request, response.Data.Memory, response.Metadata); } @@ -314,11 +320,7 @@ public override async Task InvokeBindingAsync(BindingRequest re IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var envelope = new Autogenerated.InvokeBindingRequest() - { - Name = name, - Operation = operation - }; + var envelope = new Autogenerated.InvokeBindingRequest() { Name = name, Operation = operation }; if (data != null) { @@ -340,9 +342,11 @@ public override async Task InvokeBindingAsync(BindingRequest re } catch (RpcException ex) { - throw new DaprException("Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Binding operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } } + #endregion #region InvokeMethod Apis @@ -385,7 +389,7 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); var request = new HttpRequestMessage(httpMethod, requestUri); - + request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); @@ -410,7 +414,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth /// 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, + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, + string methodName, IReadOnlyCollection> queryStringParameters, TRequest data) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); @@ -422,7 +427,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMetho return request; } - public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + public override async Task InvokeMethodWithResponseAsync(HttpRequestMessage request, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(request, nameof(request)); @@ -477,7 +483,8 @@ 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) + public async override Task InvokeMethodAsync(HttpRequestMessage request, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(request, nameof(request)); @@ -501,7 +508,8 @@ public async override Task InvokeMethodAsync(HttpRequestMessage request, Cancell } } - public async override Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + public async override Task InvokeMethodAsync(HttpRequestMessage request, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(request, nameof(request)); @@ -554,18 +562,15 @@ public async override Task InvokeMethodAsync(HttpRequestMe } } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); var envelope = new Autogenerated.InvokeServiceRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, + Id = appId, Message = new Autogenerated.InvokeRequest() { Method = methodName, }, }; var options = CreateCallOptions(headers: null, cancellationToken); @@ -580,7 +585,8 @@ public override async Task InvokeMethodGrpcAsync(string appId, string methodName } } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); @@ -590,9 +596,7 @@ public override async Task InvokeMethodGrpcAsync(string appId, string Id = appId, Message = new Autogenerated.InvokeRequest() { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), + Method = methodName, ContentType = Constants.ContentTypeApplicationGrpc, Data = Any.Pack(data), }, }; @@ -602,24 +606,22 @@ public override async Task InvokeMethodGrpcAsync(string appId, string { _ = await this.Client.InvokeServiceAsync(envelope, options); } + catch (RpcException ex) { throw new InvocationException(appId, methodName, ex); } } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, CancellationToken cancellationToken = default) + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); var envelope = new Autogenerated.InvokeServiceRequest() { - Id = appId, - Message = new Autogenerated.InvokeRequest() - { - Method = methodName, - }, + Id = appId, Message = new Autogenerated.InvokeRequest() { Method = methodName, }, }; var options = CreateCallOptions(headers: null, cancellationToken); @@ -635,7 +637,8 @@ public override async Task InvokeMethodGrpcAsync(string ap } } - public override async Task InvokeMethodGrpcAsync(string appId, string methodName, TRequest data, CancellationToken cancellationToken = default) + public override async Task InvokeMethodGrpcAsync(string appId, string methodName, + TRequest data, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNullOrEmpty(methodName, nameof(methodName)); @@ -645,9 +648,7 @@ public override async Task InvokeMethodGrpcAsync Id = appId, Message = new Autogenerated.InvokeRequest() { - Method = methodName, - ContentType = Constants.ContentTypeApplicationGrpc, - Data = Any.Pack(data), + Method = methodName, ContentType = Constants.ContentTypeApplicationGrpc, Data = Any.Pack(data), }, }; @@ -669,7 +670,9 @@ 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) + 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); @@ -681,12 +684,12 @@ public override async Task> GetBulkStateAsync(strin return bulkResponse; } - + /// public override async Task>> GetBulkStateAsync( string storeName, - IReadOnlyList keys, - int? parallelism, + IReadOnlyList keys, + int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { @@ -708,8 +711,8 @@ public override async Task>> GetBulkStateAsy /// private async Task> GetBulkStateRawAsync( string storeName, - IReadOnlyList keys, - int? parallelism, + IReadOnlyList keys, + int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { @@ -719,8 +722,7 @@ public override async Task>> GetBulkStateAsy var envelope = new Autogenerated.GetBulkStateRequest() { - StoreName = storeName, - Parallelism = parallelism ?? default + StoreName = storeName, Parallelism = parallelism ?? default }; if (metadata != null) @@ -755,7 +757,7 @@ public override async Task>> GetBulkStateAsy return bulkResponse; } - + /// public override async Task GetStateAsync( string storeName, @@ -767,11 +769,7 @@ public override async Task GetStateAsync( ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var envelope = new Autogenerated.GetStateRequest() - { - StoreName = storeName, - Key = key, - }; + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key, }; if (metadata != null) { @@ -795,7 +793,8 @@ public override async Task GetStateAsync( } 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); } try @@ -804,12 +803,15 @@ public override async Task GetStateAsync( } catch (JsonException ex) { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); } } /// - public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, CancellationToken cancellationToken = default) + public override async Task SaveBulkStateAsync(string storeName, IReadOnlyList> items, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); @@ -818,17 +820,11 @@ public override async Task SaveBulkStateAsync(string storeName, IReadOnl throw new ArgumentException("items do not contain any elements"); } - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + var envelope = new Autogenerated.SaveStateRequest() { StoreName = storeName, }; foreach (var item in items) { - var stateItem = new Autogenerated.StateItem() - { - Key = item.Key, - }; + var stateItem = new Autogenerated.StateItem() { Key = item.Key, }; if (item.ETag != null) { @@ -862,25 +858,209 @@ public override async Task SaveBulkStateAsync(string storeName, IReadOnl } 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); } + } + /// + public override async Task SaveByteStateAsync( + string storeName, + string key, + ReadOnlyMemory value, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + _ = await this.MakeSaveByteStateCallAsync( + storeName, + key, + ByteString.CopyFrom(value.Span), + etag: null, + stateOptions, + metadata, + cancellationToken); } - /// - public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, CancellationToken cancellationToken = default) + /// + public override async Task TrySaveByteStateAsync( + string storeName, + string key, + ReadOnlyMemory value, + string etag, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) { - var envelope = new Autogenerated.DeleteBulkStateRequest() + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); + return await this.MakeSaveByteStateCallAsync(storeName, key, ByteString.CopyFrom(value.Span), etag, + stateOptions, metadata, cancellationToken); + } + + // Method MakeSaveStateCallAsync to save binary value + private async Task MakeSaveByteStateCallAsync( + string storeName, + string key, + ByteString value, + string etag = default, + StateOptions stateOptions = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.SaveStateRequest() { StoreName = storeName, }; + + + var stateItem = new Autogenerated.StateItem() { Key = key, }; + + if (metadata != null) { - StoreName = storeName, - }; + foreach (var kvp in metadata) + { + stateItem.Metadata.Add(kvp.Key, kvp.Value); + } + } - foreach (var item in items) + if (etag != null) + { + stateItem.Etag = new Autogenerated.Etag() { Value = etag }; + } + + if (stateOptions != null) + { + stateItem.Options = ToAutoGeneratedStateOptions(stateOptions); + } + + if (value != null) + { + + stateItem.Value = value; + } + + envelope.States.Add(stateItem); + + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + await client.SaveStateAsync(envelope, options); + return true; + } + catch (RpcException rpc) when (etag != null && rpc.StatusCode == StatusCode.Aborted) + { + // This kind of failure indicates an ETag mismatch. Aborted doesn't seem like + // the right status code at first, but check the docs, it fits this use-case. + // + // When an ETag is used we surface this though the Try... pattern + return false; + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + + /// + public override async Task> GetByteStateAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key, }; + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } + + if (metadata != null) { - var stateItem = new Autogenerated.StateItem() + foreach (var kvp in metadata) { - Key = item.Key, - }; + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } + + var options = CreateCallOptions(headers: null, cancellationToken); + try + { + var response = await client.GetStateAsync(envelope, options); + return response.Data.ToByteArray().AsMemory(); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + } + + /// + public override async Task<(ReadOnlyMemory, string etag)> GetByteStateAndETagAsync( + string storeName, + string key, + ConsistencyMode? consistencyMode = default, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); + ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); + + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key }; + + if (metadata != null) + { + foreach (var kvp in metadata) + { + envelope.Metadata.Add(kvp.Key, kvp.Value); + } + } + + if (consistencyMode != null) + { + envelope.Consistency = GetStateConsistencyForConsistencyMode(consistencyMode.Value); + } + + var options = CreateCallOptions(headers: null, cancellationToken); + Autogenerated.GetStateResponse response; + + try + { + response = await client.GetStateAsync(envelope, options); + } + catch (RpcException ex) + { + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + + try + { + return (response.Data.ToByteArray().AsMemory(), response.Etag); + } + catch (JsonException ex) + { + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); + } + } + + /// + public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList items, + CancellationToken cancellationToken = default) + { + var envelope = new Autogenerated.DeleteBulkStateRequest() { StoreName = storeName, }; + + foreach (var item in items) + { + var stateItem = new Autogenerated.StateItem() { Key = item.Key, }; if (item.ETag != null) { @@ -909,7 +1089,8 @@ public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList< } 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); } } @@ -925,11 +1106,7 @@ public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList< ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var envelope = new Autogenerated.GetStateRequest() - { - StoreName = storeName, - Key = key - }; + var envelope = new Autogenerated.GetStateRequest() { StoreName = storeName, Key = key }; if (metadata != null) { @@ -953,16 +1130,20 @@ public override async Task DeleteBulkStateAsync(string storeName, IReadOnlyList< } 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); } try { - return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), response.Etag); + return (TypeConverters.FromJsonByteString(response.Data, this.JsonSerializerOptions), + response.Etag); } catch (JsonException ex) { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); } } @@ -1004,7 +1185,8 @@ public override async Task TrySaveStateAsync( // rely on bubbling up the error if any from Dapr runtime ArgumentVerifier.ThrowIfNull(etag, nameof(etag)); - return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, cancellationToken); + return await this.MakeSaveStateCallAsync(storeName, key, value, etag, stateOptions, metadata, + cancellationToken); } private async Task MakeSaveStateCallAsync( @@ -1016,16 +1198,10 @@ private async Task MakeSaveStateCallAsync( IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var envelope = new Autogenerated.SaveStateRequest() - { - StoreName = storeName, - }; + var envelope = new Autogenerated.SaveStateRequest() { StoreName = storeName, }; - var stateItem = new Autogenerated.StateItem() - { - Key = key, - }; + var stateItem = new Autogenerated.StateItem() { Key = key, }; if (metadata != null) { @@ -1068,7 +1244,8 @@ private async Task MakeSaveStateCallAsync( } 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); } } @@ -1100,17 +1277,13 @@ private async Task MakeExecuteStateTransactionCallAsync( IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var envelope = new Autogenerated.ExecuteStateTransactionRequest() - { - StoreName = storeName, - }; + var envelope = new Autogenerated.ExecuteStateTransactionRequest() { StoreName = storeName, }; foreach (var state in states) { var stateOperation = new Autogenerated.TransactionalStateOperation { - OperationType = state.OperationType.ToString().ToLower(), - Request = ToAutogeneratedStateItem(state) + OperationType = state.OperationType.ToString().ToLower(), Request = ToAutogeneratedStateItem(state) }; envelope.Operations.Add(stateOperation); @@ -1133,16 +1306,14 @@ private async Task MakeExecuteStateTransactionCallAsync( } 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); } } private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest state) { - var stateOperation = new Autogenerated.StateItem - { - Key = state.Key - }; + var stateOperation = new Autogenerated.StateItem { Key = state.Key }; if (state.Value != null) { @@ -1170,7 +1341,6 @@ private Autogenerated.StateItem ToAutogeneratedStateItem(StateTransactionRequest return stateOperation; } - /// public override async Task DeleteStateAsync( string storeName, @@ -1217,11 +1387,7 @@ private async Task MakeDeleteStateCallAsync( IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() - { - StoreName = storeName, - Key = key, - }; + var deleteStateEnvelope = new Autogenerated.DeleteStateRequest() { StoreName = storeName, Key = key, }; if (metadata != null) { @@ -1258,7 +1424,8 @@ private async Task MakeDeleteStateCallAsync( } 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); } } @@ -1269,11 +1436,7 @@ public async override Task> QueryStateAsync( IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var queryRequest = new Autogenerated.QueryStateRequest() - { - StoreName = storeName, - Query = jsonQuery - }; + var queryRequest = new Autogenerated.QueryStateRequest() { StoreName = storeName, Query = jsonQuery }; if (metadata != null) { @@ -1298,30 +1461,40 @@ public async override Task> QueryStateAsync( failedKeys.Add(item.Key); continue; } - items.Add(new StateQueryItem(item.Key, TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, item.Error)); + + items.Add(new StateQueryItem(item.Key, + TypeConverters.FromJsonByteString(item.Data, this.JsonSerializerOptions), item.Etag, + item.Error)); } var results = new StateQueryResponse(items, queryResponse.Token, queryResponse.Metadata); if (failedKeys.Count > 0) { // We encountered some bad keys so we throw instead of returning to alert the user. - throw new StateQueryException($"Encountered an error while processing state query results.", results, failedKeys); + throw new StateQueryException($"Encountered an error while processing state query results.", + results, failedKeys); } return results; } catch (RpcException ex) { - throw new DaprException("Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Query state operation failed: the Dapr endpointed indicated a failure. See InnerException for details.", + ex); } catch (JsonException ex) { - throw new DaprException("State operation failed: the state payload could not be deserialized. See InnerException for details.", ex); + throw new DaprException( + "State operation failed: the state payload could not be deserialized. See InnerException for details.", + ex); } } + #endregion #region Secret Apis + /// public async override Task> GetSecretAsync( string storeName, @@ -1332,11 +1505,7 @@ public async override Task> GetSecretAsync( ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); ArgumentVerifier.ThrowIfNullOrEmpty(key, nameof(key)); - var envelope = new Autogenerated.GetSecretRequest() - { - StoreName = storeName, - Key = key - }; + var envelope = new Autogenerated.GetSecretRequest() { StoreName = storeName, Key = key }; if (metadata != null) { @@ -1355,7 +1524,8 @@ public async override Task> GetSecretAsync( } catch (RpcException ex) { - throw new DaprException("Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } return response.Data.ToDictionary(kv => kv.Key, kv => kv.Value); @@ -1367,10 +1537,7 @@ public async override Task>> GetBu IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) { - var envelope = new Autogenerated.GetBulkSecretRequest() - { - StoreName = storeName - }; + var envelope = new Autogenerated.GetBulkSecretRequest() { StoreName = storeName }; if (metadata != null) { @@ -1389,14 +1556,18 @@ public async override Task>> GetBu } catch (RpcException ex) { - throw new DaprException("Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Bulk secret operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } return response.Data.ToDictionary(r => r.Key, r => r.Value.Secrets.ToDictionary(s => s.Key, s => s.Value)); } + #endregion #region Configuration API + /// public async override Task GetConfiguration( string storeName, @@ -1406,10 +1577,7 @@ public async override Task GetConfiguration( { ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); - var request = new Autogenerated.GetConfigurationRequest() - { - StoreName = storeName - }; + var request = new Autogenerated.GetConfigurationRequest() { StoreName = storeName }; if (keys != null && keys.Count > 0) { @@ -1432,10 +1600,13 @@ public async override Task GetConfiguration( } catch (RpcException ex) { - throw new DaprException("GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "GetConfiguration operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } - var responseItems = response.Items.ToDictionary(item => item.Key, item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); + var responseItems = response.Items.ToDictionary(item => item.Key, + item => new ConfigurationItem(item.Value.Value, item.Value.Version, item.Value.Metadata)); return new GetConfigurationResponse(responseItems); } @@ -1468,7 +1639,8 @@ public override Task SubscribeConfiguration( } var options = CreateCallOptions(headers: null, cancellationToken: cancellationToken); - return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); + return Task.FromResult(new SubscribeConfigurationResponse( + new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); } public override async Task UnsubscribeConfiguration( @@ -1479,11 +1651,8 @@ public override async Task UnsubscribeConfigur ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(id)); - Autogenerated.UnsubscribeConfigurationRequest request = new Autogenerated.UnsubscribeConfigurationRequest() - { - StoreName = storeName, - Id = id - }; + Autogenerated.UnsubscribeConfigurationRequest request = + new Autogenerated.UnsubscribeConfigurationRequest() { StoreName = storeName, Id = id }; var options = CreateCallOptions(headers: null, cancellationToken); var resp = await client.UnsubscribeConfigurationAsync(request, options); @@ -1495,32 +1664,37 @@ public override async Task UnsubscribeConfigur #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, + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) { if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) { - var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), + keyName, encryptionOptions, cancellationToken); - + var bufferedResult = new ArrayBufferWriter(); await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) { bufferedResult.Write(item.Span); } - - return bufferedResult.WrittenMemory; + + return bufferedResult.WrittenMemory; } - throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", + nameof(plaintextBytes)); } /// - [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, + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> EncryptAsync(string vaultResourceName, + Stream plaintextStream, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); @@ -1528,7 +1702,9 @@ public override async Task>> EncryptAsync( ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); - var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + var shouldOmitDecryptionKeyName = + string.IsNullOrWhiteSpace(encryptionOptions + .DecryptionKeyName); //Whitespace isn't likely a valid key name either var encryptRequestOptions = new Autogenerated.EncryptRequestOptions { @@ -1541,7 +1717,8 @@ public override async Task>> EncryptAsync( if (!shouldOmitDecryptionKeyName) { - ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, + nameof(encryptionOptions.DecryptionKeyName)); encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; } @@ -1569,7 +1746,7 @@ private async Task SendPlaintextStreamAsync(Stream plaintextStream, { //Start with passing the metadata about the encryption request itself in the first message await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest {Options = encryptRequestOptions}, cancellationToken); + new Autogenerated.EncryptRequest { Options = encryptRequestOptions }, cancellationToken); //Send the plaintext bytes in blocks in subsequent messages await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) @@ -1579,7 +1756,8 @@ await duplexStream.RequestStream.WriteAsync( ulong sequenceNumber = 0; while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken)) != 0) { await duplexStream.RequestStream.WriteAsync( @@ -1603,7 +1781,9 @@ await duplexStream.RequestStream.WriteAsync( /// /// 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) + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) .ConfigureAwait(false)) @@ -1611,10 +1791,12 @@ private async IAsyncEnumerable> RetrieveEncryptedStreamAsyn yield return encryptResponse.Payload.Data.Memory; } } - + /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); @@ -1624,8 +1806,7 @@ public override async Task>> DecryptAsync( var decryptRequestOptions = new Autogenerated.DecryptRequestOptions { - ComponentName = vaultResourceName, - KeyName = keyName + ComponentName = vaultResourceName, KeyName = keyName }; var options = CreateCallOptions(headers: null, cancellationToken); @@ -1644,12 +1825,13 @@ public override async Task>> DecryptAsync( } /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [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. /// @@ -1662,30 +1844,32 @@ private async Task SendCiphertextStreamAsync(Stream ciphertextStream, //Start with passing the metadata about the decryption request itself in the first message await duplexStream.RequestStream.WriteAsync( new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); - + //Send the ciphertext bytes in blocks in subsequent messages await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) { var buffer = new byte[streamingBlockSizeInBytes]; int bytesRead; ulong sequenceNumber = 0; - - while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken)) != 0) { - await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest - { - Payload = new Autogenerated.StreamPayload + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), - Seq = sequenceNumber - } - }, cancellationToken); - + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + //Increment the sequence number sequenceNumber++; } } - + //Send the completion message await duplexStream.RequestStream.CompleteAsync(); } @@ -1703,9 +1887,10 @@ private async IAsyncEnumerable> RetrieveDecryptedStreamAsyn yield return decryptResponse.Payload.Data.Memory; } } - + /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [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) @@ -1713,8 +1898,8 @@ public override async Task> DecryptAsync(string vaultResour if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) { var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), - keyName, decryptionOptions, cancellationToken); - + keyName, decryptionOptions, cancellationToken); + var bufferedResult = new ArrayBufferWriter(); await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) { @@ -1724,16 +1909,18 @@ public override async Task> DecryptAsync(string vaultResour return bufferedResult.WrittenMemory; } - throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + throw new ArgumentException("The input instance doesn't have a valid underlying data store", + nameof(ciphertextBytes)); } /// - [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [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 ///// @@ -1984,6 +2171,7 @@ await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, #endregion #region Distributed Lock API + /// [Obsolete] public async override Task Lock( @@ -2004,27 +2192,23 @@ public async override Task Lock( var request = new Autogenerated.TryLockRequest() { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - ExpiryInSeconds = expiryInSeconds + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner, ExpiryInSeconds = expiryInSeconds }; try { var options = CreateCallOptions(headers: null, cancellationToken); + var response = await client.TryLockAlpha1Async(request, options); return new TryLockResponse() { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner, - Success = response.Success + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner, Success = response.Success }; } catch (RpcException ex) { - throw new DaprException("Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Lock operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); } } @@ -2042,9 +2226,7 @@ public async override Task Unlock( var request = new Autogenerated.UnlockRequest() { - StoreName = storeName, - ResourceId = resourceId, - LockOwner = lockOwner + StoreName = storeName, ResourceId = resourceId, LockOwner = lockOwner }; var options = CreateCallOptions(headers: null, cancellationToken); @@ -2055,7 +2237,8 @@ public async override Task Unlock( } catch (RpcException ex) { - throw new DaprException("Lock 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)); @@ -2078,7 +2261,8 @@ public override async Task CheckHealthAsync(CancellationToken cancellation try { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + using var response = + await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return response.IsSuccessStatusCode; } catch (HttpRequestException) @@ -2100,7 +2284,8 @@ public override async Task CheckOutboundHealthAsync(CancellationToken canc try { - using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + using var response = + await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); return response.IsSuccessStatusCode; } catch (HttpRequestException) @@ -2119,6 +2304,7 @@ public override async Task WaitForSidecarAsync(CancellationToken cancellationTok { break; } + await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken); } } @@ -2147,20 +2333,19 @@ public override async Task GetMetadataAsync(CancellationToken canc } catch (RpcException ex) { - throw new DaprException("Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Get metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } } /// - public override async Task SetMetadataAsync(string attributeName, string attributeValue, CancellationToken cancellationToken = default) + public override async Task SetMetadataAsync(string attributeName, string attributeValue, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(attributeName, nameof(attributeName)); - var envelope = new Autogenerated.SetMetadataRequest() - { - Key = attributeName, - Value = attributeValue - }; + var envelope = new Autogenerated.SetMetadataRequest() { Key = attributeName, Value = attributeValue }; var options = CreateCallOptions(headers: null, cancellationToken); @@ -2170,9 +2355,12 @@ public override async Task SetMetadataAsync(string attributeName, string attribu } catch (RpcException ex) { - throw new DaprException("Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "Set metadata operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } } + #endregion protected override void Dispose(bool disposing) @@ -2209,7 +2397,8 @@ private CallOptions CreateCallOptions(Metadata headers, CancellationToken cancel /// /// /// - private async Task MakeGrpcCallHandleError(Func> callFunc, CancellationToken cancellationToken = default) + private async Task MakeGrpcCallHandleError( + Func> callFunc, CancellationToken cancellationToken = default) { var callOptions = CreateCallOptions(headers: null, cancellationToken); return await callFunc.Invoke(callOptions); @@ -2232,7 +2421,8 @@ private Autogenerated.StateOptions ToAutoGeneratedStateOptions(StateOptions stat return stateRequestOptions; } - private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode(ConsistencyMode consistencyMode) + private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsistencyForConsistencyMode( + ConsistencyMode consistencyMode) { return consistencyMode switch { @@ -2242,7 +2432,8 @@ private static Autogenerated.StateOptions.Types.StateConsistency GetStateConsist }; } - private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode(ConcurrencyMode concurrencyMode) + private static Autogenerated.StateOptions.Types.StateConcurrency GetStateConcurrencyForConcurrencyMode( + ConcurrencyMode concurrencyMode) { return concurrencyMode switch { 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.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs index 60a9827a2..7a7abf025 100644 --- a/src/Dapr.Common/DaprGenericClientBuilder.cs +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -199,8 +199,8 @@ public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); } - - var httpEndpoint = new Uri(this.HttpEndpoint); + + var httpEndpoint = new Uri(this.HttpEndpoint); if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") { throw new InvalidOperationException("The HTTP endpoint must use http or https."); diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs index 1f035220e..b548290df 100644 --- a/src/Dapr.Jobs/DaprJobsGrpcClient.cs +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -47,7 +47,7 @@ internal sealed class DaprJobsGrpcClient : DaprJobsClient /// Property exposed for testing purposes. /// internal Autogenerated.Dapr.DaprClient Client { get; } - + internal DaprJobsGrpcClient( Autogenerated.Dapr.DaprClient innerClient, HttpClient httpClient, diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs index e3680fd83..03540aae1 100644 --- a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -30,6 +30,7 @@ public static class DaprJobsServiceCollectionExtensions /// The lifetime of the registered services. /// public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null, ServiceLifetime lifetime = ServiceLifetime.Singleton) + { ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); @@ -62,7 +63,7 @@ public static IServiceCollection AddDaprJobsClient(this IServiceCollection servi serviceCollection.TryAddSingleton(registration); break; } - + return serviceCollection; } } diff --git a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto index 0ab371e6d..d1d8658f3 100644 --- a/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto +++ b/src/Dapr.Protos/Protos/dapr/proto/runtime/v1/dapr.proto @@ -1225,6 +1225,7 @@ message Job { // Systemd timer style cron accepts 6 fields: // seconds | minutes | hours | day of month | month | day of week // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-6/sun-sat + // // "0 30 * * * *" - every hour on the half hour // "0 15 3 * * *" - every day at 03:15 @@ -1344,4 +1345,4 @@ message ConversationResponse { // An array of results. repeated ConversationResult outputs = 2; -} \ No newline at end of file +} 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/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index f6ecb5d80..12fd0e3de 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -11,24 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; +using FluentAssertions; +using Google.Protobuf; +using Grpc.Core; +using Moq; +using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency; +using StateConcurrency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConcurrency; +using Xunit; +using System.Threading; +using System.Net.Http; +using System.Text; + namespace Dapr.Client.Test { - using System; - using System.Collections.Generic; - using System.Net; - using System.Text.Json; - using System.Threading.Tasks; - using Autogenerated = Dapr.Client.Autogen.Grpc.v1; - using FluentAssertions; - using Google.Protobuf; - using Grpc.Core; - using Moq; - using StateConsistency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConsistency; - using StateConcurrency = Dapr.Client.Autogen.Grpc.v1.StateOptions.Types.StateConcurrency; - using Xunit; - using System.Threading; - using System.Net.Http; - public class StateApiTest { [Fact] @@ -36,10 +38,7 @@ public async Task GetStateAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); request.Dismiss(); @@ -58,14 +57,11 @@ public async Task GetBulkStateAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null)); // Create Response & Respond - var data = "value"; + const string data = "value"; var envelope = MakeGetBulkStateResponse(key, data); var state = await request.CompleteWithMessageAsync(envelope); @@ -78,11 +74,8 @@ public async Task GetBulkStateAsync_CanReadDeserializedState() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() {key}, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() {key}, null)); // Create Response & Respond const string size = "small"; @@ -102,11 +95,8 @@ public async Task GetBulkStateAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null); - }); + const string key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null)); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -121,15 +111,12 @@ public async Task GetBulkStateAsync_ValidateRequest() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; + const string key = "test"; var metadata = new Dictionary { { "partitionKey", "mypartition" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetBulkStateAsync("testStore", new List() { key }, null, metadata: metadata)); request.Dismiss(); @@ -144,10 +131,7 @@ public async Task GetStateAndEtagAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -165,10 +149,7 @@ public async Task GetStateAndETagAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -183,10 +164,7 @@ public async Task GetStateAndETagAsync_WrapsJsonException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAndETagAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAndETagAsync("testStore", "test")); // Create Response & Respond var envelope = new Autogenerated.GetStateResponse() @@ -206,10 +184,7 @@ public async Task GetStateAsync_CanReadEmptyState_ReturnsDefault() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", ConsistencyMode.Eventual)); // Create Response & Respond var envelope = MakeGetStateResponse(null); @@ -226,10 +201,7 @@ public async Task GetStateAsync_ValidateRequest(ConsistencyMode consistencyMode, { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", consistencyMode); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", consistencyMode)); // Get Request & Validate var envelope = await request.GetRequestEnvelopeAsync(); @@ -253,10 +225,7 @@ public async Task GetStateAndEtagAsync_ValidateRequest() { { "partitionKey", "mypartition" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test", metadata: metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test", metadata: metadata)); // Get Request & Validate var envelope = await request.GetRequestEnvelopeAsync(); @@ -276,10 +245,7 @@ public async Task GetStateAsync_WrapsRpcException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); // Create Response & Respond var ex = await Assert.ThrowsAsync(async () => @@ -294,10 +260,7 @@ public async Task GetStateAsync_WrapsJsonException() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateAsync("testStore", "test")); // Create Response & Respond var stateResponse = new Autogenerated.GetStateResponse() @@ -467,10 +430,10 @@ public async Task ExecuteStateTransactionAsync_CanSaveState() }; var state1 = new StateTransactionRequest("stateKey1", JsonSerializer.SerializeToUtf8Bytes(stateValue1), StateOperationType.Upsert, "testEtag", metadata1, options1); - var stateValue2 = 100; + const int stateValue2 = 100; var state2 = new StateTransactionRequest("stateKey2", JsonSerializer.SerializeToUtf8Bytes(stateValue2), StateOperationType.Delete); - var stateValue3 = "teststring"; + const string stateValue3 = "teststring"; var state3 = new StateTransactionRequest("stateKey3", JsonSerializer.SerializeToUtf8Bytes(stateValue3), StateOperationType.Upsert); var states = new List @@ -619,10 +582,7 @@ public async Task GetStateEntryAsync_CanReadState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -639,10 +599,7 @@ public async Task GetStateEntryAsync_CanReadEmptyState_ReturnsDefault() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var envelope = MakeGetStateResponse(null); @@ -657,10 +614,7 @@ public async Task GetStateEntryAsync_CanSaveState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -699,10 +653,7 @@ public async Task GetStateEntryAsync_CanDeleteState() { await using var client = TestClient.CreateForDaprClient(); - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.GetStateEntryAsync("testStore", "test"); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetStateEntryAsync("testStore", "test")); // Create Response & Respond var data = new Widget() { Size = "small", Color = "yellow", }; @@ -805,10 +756,7 @@ public async Task TrySaveStateAsync_ValidateOptions( { "key1", "value1" }, { "key2", "value2" } }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TrySaveStateAsync("testStore", "test", widget, "Test_Etag", stateOptions, metadata)); request.Dismiss(); @@ -1021,10 +969,7 @@ public async Task TryDeleteStateAsync_ValidateOptions( Consistency = consistencyMode }; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions); - }); + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TryDeleteStateAsync("testStore", "test", "Test_Etag", stateOptions)); request.Dismiss(); @@ -1042,8 +987,8 @@ public async Task DeleteBulkStateAsync_ValidateRequest() { await using var client = TestClient.CreateForDaprClient(); - var key = "test"; - var etag = "etag"; + const string key = "test"; + const string etag = "etag"; var metadata = new Dictionary { { "partitionKey", "mypartition" } @@ -1069,11 +1014,8 @@ public async Task QueryStateAsync_ValidateResult() { await using var client = TestClient.CreateForDaprClient(); - var queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary()); - }); + const string queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary())); // Validate request. var envelope = await request.GetRequestEnvelopeAsync(); @@ -1099,11 +1041,8 @@ public async Task QueryStateAsync_EncountersError_ValidatePartialResult() { await using var client = TestClient.CreateForDaprClient(); - var queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; - var request = await client.CaptureGrpcRequestAsync(async daprClient => - { - return await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary()); - }); + const string queryJson = "{'query':{'filter':{ 'EQ': {'value':'test'}}}}"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.QueryStateAsync("testStore", queryJson, new Dictionary())); // Validate request. var envelope = await request.GetRequestEnvelopeAsync(); @@ -1175,14 +1114,371 @@ private Autogenerated.GetBulkStateResponse MakeGetBulkStateResponse(string ke private Autogenerated.QueryStateItem MakeQueryStateItem(string key, T data, string etag = default, string error = default) { - var wireItem = new Autogenerated.QueryStateItem(); - wireItem.Key = key; - wireItem.Data = ByteString.CopyFromUtf8(JsonSerializer.Serialize(data)); - wireItem.Etag = etag ?? string.Empty; - wireItem.Error = error ?? string.Empty; + var wireItem = new Autogenerated.QueryStateItem + { + Key = key, Data = ByteString.CopyFromUtf8(JsonSerializer.Serialize(data)), Etag = etag ?? string.Empty, + Error = error ?? string.Empty + }; return wireItem; } + [Theory] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyLastWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyLastWrite)] + public async Task SaveByteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), stateOptions, metadata); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + state.Metadata.Count.Should().Be(2); + state.Metadata.Keys.Contains("key1").Should().BeTrue(); + state.Metadata.Keys.Contains("key2").Should().BeTrue(); + state.Metadata["key1"].Should().Be("value1"); + state.Metadata["key2"].Should().Be("value2"); + state.Options.Concurrency.Should().Be(expectedConcurrency); + state.Options.Consistency.Should().Be(expectedConsistency); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task SaveByteStateAsync_CanSaveState() + { + await using var client = TestClient.CreateForDaprClient(); + + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", stateBytes.AsMemory()); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task SaveByteStateAsync_CanClearState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.SaveByteStateAsync("testStore", "test", null); + }); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Key.Should().Be("test"); + state.Value.Should().Equal(ByteString.Empty); + } + + [Fact] + public async Task SaveByteStateAsync_WithCancelledToken() + { + await using var client = TestClient.CreateForDaprClient(); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync(async () => + { + await client.InnerClient.SaveByteStateAsync("testStore", "test", null, cancellationToken: cts.Token); + }); + } + + [Theory] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Eventual, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyEventual, StateConcurrency.ConcurrencyLastWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.FirstWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyFirstWrite)] + [InlineData(ConsistencyMode.Strong, ConcurrencyMode.LastWrite, StateConsistency.ConsistencyStrong, StateConcurrency.ConcurrencyLastWrite)] + public async Task TrySaveByteStateAsync_ValidateOptions( + ConsistencyMode consistencyMode, + ConcurrencyMode concurrencyMode, + StateConsistency expectedConsistency, + StateConcurrency expectedConcurrency) + { + await using var client = TestClient.CreateForDaprClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var stateOptions = new StateOptions + { + Concurrency = concurrencyMode, + Consistency = consistencyMode + }; + + var metadata = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.TrySaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), "Test_Etag", stateOptions, metadata)); + + request.Dismiss(); + + // Get Request and validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.States.Count.Should().Be(1); + var state = envelope.States[0]; + state.Etag.Value.Should().Be("Test_Etag"); + state.Metadata.Count.Should().Be(2); + state.Metadata.Keys.Contains("key1").Should().BeTrue(); + state.Metadata.Keys.Contains("key2").Should().BeTrue(); + state.Metadata["key1"].Should().Be("value1"); + state.Metadata["key2"].Should().Be("value2"); + state.Options.Concurrency.Should().Be(expectedConcurrency); + state.Options.Consistency.Should().Be(expectedConsistency); + + var stateBinaryData = state.Value.ToStringUtf8(); + stateBinaryData.Should().Be(data); + } + + [Fact] + public async Task TrySaveByteStateAsync_ValidateNonETagErrorThrowsException() + { + var client = new MockClient(); + + var response = client.CallStateApi() + .Build(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var rpcException = new RpcException(new Status(StatusCode.Internal, "Network Error")); + + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var ex = await Assert.ThrowsAsync(async () => + { + await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), "someETag"); + }); + Assert.Same(rpcException, ex.InnerException); + } + + [Fact] + public async Task TrySaveByteStateAsync_ValidateETagRelatedExceptionReturnsFalse() + { + var client = new MockClient(); + + var response = client.CallStateApi() + .Build(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var rpcException = new RpcException(new Status(StatusCode.Aborted, $"failed saving state in state store testStore")); + // Setup the mock client to throw an Rpc Exception with the expected details info + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Throws(rpcException); + + var operationResult = await client.DaprClient.TrySaveByteStateAsync("testStore", "test", stateBytes.AsMemory(), "invalidETag"); + Assert.False(operationResult); + } + + [Fact] + public async Task TrySaveByteStateAsync_NullEtagThrowsArgumentException() + { + var client = new MockClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var response = client.CallStateApi() + .Build(); + + await FluentActions.Awaiting(async () => await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), null)) + .Should().ThrowAsync(); + } + + [Fact] + public async Task TrySaveByteStateAsync_EmptyEtagDoesNotThrow() + { + var client = new MockClient(); + const string data = "Test binary data"; + var stateBytes = Encoding.UTF8.GetBytes(data); + var response = client.CallStateApi() + .Build(); + + // Setup the mock client to return success + client.Mock + .Setup(m => m.SaveStateAsync(It.IsAny(), It.IsAny())) + .Returns(response); + + var result = await client.DaprClient.TrySaveByteStateAsync("test", "test", stateBytes.AsMemory(), ""); + Assert.True(result); + } + [Fact] + public async Task GetByteStateAsync_CanReadEmptyState_ReturnsDefault() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test", ConsistencyMode.Eventual)); + + // Create Response & Respond to request + var envelope = MakeGetByteStateResponse(null); + var state = await request.CompleteWithMessageAsync(envelope); + + // Get response and validate + state.ToArray().Should().BeNullOrEmpty(); + } + + [Theory] + [InlineData(ConsistencyMode.Eventual, StateConsistency.ConsistencyEventual)] + [InlineData(ConsistencyMode.Strong, StateConsistency.ConsistencyStrong)] + public async Task GetByteStateAsync_ValidateRequest(ConsistencyMode consistencyMode, StateConsistency expectedConsistencyMode) + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test", consistencyMode)); + + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.Key.Should().Be("test"); + envelope.Consistency.Should().Be(expectedConsistencyMode); + var binaryData = Encoding.ASCII.GetBytes("test data"); + // Create Response & Respond + var state = await request.CompleteWithMessageAsync(MakeGetByteStateResponse(binaryData.AsMemory())); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + } + + [Fact] + public async Task GetByteStateAndEtagAsync_ValidateRequest() + { + await using var client = TestClient.CreateForDaprClient(); + + var metadata = new Dictionary + { + { "partitionKey", "mypartition" } + }; + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test", metadata: metadata)); + + // Get Request & Validate + var envelope = await request.GetRequestEnvelopeAsync(); + envelope.StoreName.Should().Be("testStore"); + envelope.Key.Should().Be("test"); + envelope.Metadata.Should().BeEquivalentTo(metadata); + var binaryData = Encoding.ASCII.GetBytes("test data"); + // Create Response & Respond + var (state, etag) = await request.CompleteWithMessageAsync((MakeGetByteStateResponse(binaryData.AsMemory()))); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + } + [Fact] + public async Task GetByteStateAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } + + [Fact] + public async Task GetByteStateAndEtagAsync_CanReadState() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var binaryData = Encoding.ASCII.GetBytes("test data"); + var envelope = MakeGetByteStateResponse(binaryData.AsMemory(), "Test_Etag"); + var (state, etag) = await request.CompleteWithMessageAsync(envelope); + var stateStr = ByteString.CopyFrom(state.Span).ToByteArray(); + // Get response and validate + stateStr.Should().BeEquivalentTo(binaryData); + etag.Should().Be("Test_Etag"); + } + + [Fact] + public async Task GetByteStateAndETagAsync_WrapsRpcException() + { + await using var client = TestClient.CreateForDaprClient(); + + var request = await client.CaptureGrpcRequestAsync(async daprClient => await daprClient.GetByteStateAndETagAsync("testStore", "test")); + + // Create Response & Respond + var ex = await Assert.ThrowsAsync(async () => + { + await request.CompleteAsync(new HttpResponseMessage(HttpStatusCode.NotAcceptable)); + }); + Assert.IsType(ex.InnerException); + } + + private Autogenerated.GetStateResponse MakeGetByteStateResponse(ReadOnlyMemory state, string etag = null) + { + + var response = new Autogenerated.GetStateResponse(); + + // convert to byte string if state is not null + if (!state.Span.IsEmpty) + { + response.Data = ByteString.CopyFrom(state.Span); + } + + if (etag != null) + { + response.Etag = etag; + } + + return response; + } private class Widget { public string Size { get; set; } diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs index 3b2c5f990..28a8a0681 100644 --- a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTests.cs @@ -25,6 +25,7 @@ namespace Dapr.Jobs.Test.Extensions; public class DaprJobsServiceCollectionExtensionsTest { [Fact] + public void AddDaprJobsClient_FromIConfiguration() { const string apiToken = "abc123"; @@ -50,6 +51,7 @@ public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() { var services = new ServiceCollection(); + var clientBuilder = new Action((sp, builder) => { builder.UseDaprApiToken("abc"); From dd06c48c926aeb1bca762761eb60f4026fc1742e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 11 Dec 2024 15:29:39 -0600 Subject: [PATCH 52/69] Fixes + unit tests for streaming PubSub implementation (#1415) * Added null check - the proto suggests this shouldn't ever be null, but there's an issue reporting as much, so this fixes that Signed-off-by: Whit Waldo * Removed the Task.WhenAll making the operation non-blocking Signed-off-by: Whit Waldo * Added unit test to validate that the subscription is no longer blocking Signed-off-by: Whit Waldo * Removed unused line from previous test, added another test Signed-off-by: Whit Waldo * Added another test Signed-off-by: Whit Waldo * More unit tests Signed-off-by: Whit Waldo * Added more unit tests Signed-off-by: Whit Waldo * Updated to make DaprPublishSubscribeClientBuilder configurable via a registered IConfiguration Signed-off-by: Whit Waldo * Added missing copyright statements Signed-off-by: Whit Waldo * Added missing package reference Signed-off-by: Whit Waldo * Fixed bad reference (missed in merge) Signed-off-by: Whit Waldo * Fixed failing unit test Signed-off-by: Whit Waldo * Tweak to only pass along EventMessage payloads to developers as it's expected that the initial response will be null if EventMessage is populated Signed-off-by: Whit Waldo * Was missing assignment of the Data property in the TopicMessage. Shout out to both @tommorvolloriddle and @Aimless321 for catching this! Signed-off-by: Whit Waldo * Fix - return would be bad. Continue is the right move. Signed-off-by: Whit Waldo * Added a simple test Signed-off-by: Whit Waldo * Fixed unit tests Signed-off-by: Whit Waldo * Merged in tweaks from #1422 Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Messaging/AssemblyInfo.cs | 18 ++ .../DaprPublishSubscribeGrpcClient.cs | 4 +- .../PublishSubscribeReceiver.cs | 57 ++++- .../Dapr.Messaging.Test.csproj | 1 + ...bscribeServiceCollectionExtensionsTests.cs | 74 ++++++- .../MessageHandlingPolicyTest.cs | 15 +- .../PublishSubscribeReceiverTests.cs | 206 ++++++++++++++++++ 7 files changed, 358 insertions(+), 17 deletions(-) create mode 100644 src/Dapr.Messaging/AssemblyInfo.cs create mode 100644 test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs diff --git a/src/Dapr.Messaging/AssemblyInfo.cs b/src/Dapr.Messaging/AssemblyInfo.cs new file mode 100644 index 000000000..4e2e7a0a7 --- /dev/null +++ b/src/Dapr.Messaging/AssemblyInfo.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Messaging.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + + diff --git a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs index 39024cb35..33ef05494 100644 --- a/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs +++ b/src/Dapr.Messaging/PublishSubscribe/DaprPublishSubscribeGrpcClient.cs @@ -47,7 +47,7 @@ internal sealed class DaprPublishSubscribeGrpcClient : DaprPublishSubscribeClien ///
public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) { - Client = client; + this.Client = client; this.HttpClient = httpClient; this.DaprApiToken = daprApiToken; } @@ -63,7 +63,7 @@ public DaprPublishSubscribeGrpcClient(P.DaprClient client, HttpClient httpClient /// public override async Task SubscribeAsync(string pubSubName, string topicName, DaprSubscriptionOptions options, TopicMessageHandler messageHandler, CancellationToken cancellationToken = default) { - var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, Client); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, this.Client); await receiver.SubscribeAsync(cancellationToken); return receiver; } diff --git a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs index 886d57006..4b0d608ff 100644 --- a/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs +++ b/src/Dapr.Messaging/PublishSubscribe/PublishSubscribeReceiver.cs @@ -77,6 +77,11 @@ internal sealed class PublishSubscribeReceiver : IAsyncDisposable ///
private bool isDisposed; + // Internal property for testing purposes + internal Task TopicMessagesChannelCompletion => topicMessagesChannel.Reader.Completion; + // Internal property for testing purposes + internal Task AcknowledgementsChannelCompletion => acknowledgementsChannel.Reader.Completion; + /// /// Constructs a new instance of a instance. /// @@ -115,20 +120,40 @@ internal async Task SubscribeAsync(CancellationToken cancellationToken = default var stream = await GetStreamAsync(cancellationToken); - //Retrieve the messages from the sidecar and write to the messages channel - var fetchMessagesTask = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken); + //Retrieve the messages from the sidecar and write to the messages channel - start without awaiting so this isn't blocking + _ = FetchDataFromSidecarAsync(stream, topicMessagesChannel.Writer, cancellationToken) + .ContinueWith(HandleTaskCompletion, null, cancellationToken, TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); //Process the messages as they're written to either channel - var acknowledgementProcessorTask = ProcessAcknowledgementChannelMessagesAsync(stream, cancellationToken); - var topicMessageProcessorTask = ProcessTopicChannelMessagesAsync(cancellationToken); + _ = ProcessAcknowledgementChannelMessagesAsync(stream, cancellationToken).ContinueWith(HandleTaskCompletion, + null, cancellationToken, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + _ = ProcessTopicChannelMessagesAsync(cancellationToken).ContinueWith(HandleTaskCompletion, null, + cancellationToken, + TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + } - try - { - await Task.WhenAll(fetchMessagesTask, acknowledgementProcessorTask, topicMessageProcessorTask); - } - catch (OperationCanceledException) + /// + /// Exposed for testing purposes only. + /// + /// The test message to write. + internal async Task WriteMessageToChannelAsync(TopicMessage message) + { + await topicMessagesChannel.Writer.WriteAsync(message); + } + + //Exposed for testing purposes only + internal async Task WriteAcknowledgementToChannelAsync(TopicAcknowledgement acknowledgement) + { + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); + } + + //Exposed for testing purposes only + internal static void HandleTaskCompletion(Task task, object? state) + { + if (task.Exception != null) { - // Will be cleaned up during DisposeAsync + throw task.Exception; } } @@ -251,13 +276,21 @@ await stream.RequestStream.WriteAsync( //Each time a message is received from the stream, push it into the topic messages channel await foreach (var response in stream.ResponseStream.ReadAllAsync(cancellationToken)) { + //https://github.com/dapr/dotnet-sdk/issues/1412 reports that this is sometimes null + //Skip the initial response - we only want to pass along TopicMessage payloads to developers + if (response?.EventMessage is null) + { + continue; + } + var message = new TopicMessage(response.EventMessage.Id, response.EventMessage.Source, response.EventMessage.Type, response.EventMessage.SpecVersion, response.EventMessage.DataContentType, response.EventMessage.Topic, response.EventMessage.PubsubName) { Path = response.EventMessage.Path, - Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value) + Extensions = response.EventMessage.Extensions.Fields.ToDictionary(f => f.Key, kvp => kvp.Value), + Data = response.EventMessage.Data.ToByteArray() }; try @@ -308,6 +341,6 @@ public async ValueTask DisposeAsync() ///
/// The identifier of the message. /// The action to take on the message in the acknowledgement request. - private sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); + internal sealed record TopicAcknowledgement(string MessageId, TopicEventResponse.Types.TopicEventResponseStatus Action); } diff --git a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj index 8f39e1713..0b10230f7 100644 --- a/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj +++ b/test/Dapr.Messaging.Test/Dapr.Messaging.Test.csproj @@ -25,6 +25,7 @@ + diff --git a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs index d239fb86d..d8e218d52 100644 --- a/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs +++ b/test/Dapr.Messaging.Test/Extensions/PublishSubscribeServiceCollectionExtensionsTests.cs @@ -1,20 +1,90 @@ -using Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Messaging.PublishSubscribe; using Dapr.Messaging.PublishSubscribe.Extensions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Moq; namespace Dapr.Messaging.Test.Extensions; public sealed class PublishSubscribeServiceCollectionExtensionsTests { [Fact] - public void AddDaprPubSubClient_RegistersIHttpClientFactory() + public void AddDaprMessagingClient_FromIConfiguration() { + const string apiToken = "abc123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"DAPR_API_TOKEN", apiToken } + }) + .Build(); + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprPubSubClient(); + + var app = services.BuildServiceProvider(); + + var pubSubClient = app.GetRequiredService() as DaprPublishSubscribeGrpcClient; + + Assert.NotNull(pubSubClient!); + Assert.Equal(apiToken, pubSubClient.DaprApiToken); + } + + [Fact] + public void AddDaprPubSubClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); services.AddDaprPubSubClient(); var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + Assert.NotNull(daprClient); + } + + [Fact] + public void AddDaprPubSubClient_CallsConfigureAction() + { + var services = new ServiceCollection(); + + var configureCalled = false; + + services.AddDaprPubSubClient(Configure); + + var serviceProvider = services.BuildServiceProvider(); + var daprClient = serviceProvider.GetService(); + Assert.NotNull(daprClient); + Assert.True(configureCalled); + return; + + void Configure(IServiceProvider sp, DaprPublishSubscribeClientBuilder builder) + { + configureCalled = true; + } + } + [Fact] + public void AddDaprPubSubClient_RegistersServicesCorrectly() + { + var services = new ServiceCollection(); + services.AddDaprPubSubClient(); + var serviceProvider = services.BuildServiceProvider(); + var httpClientFactory = serviceProvider.GetService(); Assert.NotNull(httpClientFactory); diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs index 0efb5e879..6efdd6397 100644 --- a/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs +++ b/test/Dapr.Messaging.Test/PublishSubscribe/MessageHandlingPolicyTest.cs @@ -1,4 +1,17 @@ -using Dapr.Messaging.PublishSubscribe; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Messaging.PublishSubscribe; namespace Dapr.Messaging.Test.PublishSubscribe { diff --git a/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs new file mode 100644 index 000000000..f8070aa66 --- /dev/null +++ b/test/Dapr.Messaging.Test/PublishSubscribe/PublishSubscribeReceiverTests.cs @@ -0,0 +1,206 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Threading.Channels; +using Dapr.AppCallback.Autogen.Grpc.v1; +using Dapr.Messaging.PublishSubscribe; +using Grpc.Core; +using Moq; +using P = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Messaging.Test.PublishSubscribe; + +public class PublishSubscribeReceiverTests +{ + [Fact] + public void SubscribeAsync_ShouldNotBlock() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); + + //Mock the daprClient + var mockDaprClient = new Mock(); + + //Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = + new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), + () => new Status(), () => new Metadata(), () => { }); + + //Setup the mock to return the mock call + mockDaprClient.Setup(client => + client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var subscribeTask = receiver.SubscribeAsync(); + stopwatch.Stop(); + + Assert.True(stopwatch.ElapsedMilliseconds < 100, "SubscribeAsync should return immediately and not block"); + } + + [Fact] + public void Constructor_ShouldInitializeCorrectly() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, token) => Task.FromResult(TopicResponseAction.Success)); + + //Mock the daprClient + var mockDaprClient = new Mock(); + var receiver = + new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, mockDaprClient.Object); + Assert.NotNull(receiver); + } + + [Fact] + public async Task ProcessTopicChannelMessagesAsync_ShouldProcessMessages() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + // Mock the message handler + var mockMessageHandler = new Mock(); + mockMessageHandler + .Setup(handler => handler(It.IsAny(), It.IsAny())) + .ReturnsAsync(TopicResponseAction.Success); + + //Mock the daprClient + var mockDaprClient = new Mock(); + // Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), () => new Status(), () => new Metadata(), () => { }); + + //Set up the mock to return the mock call + mockDaprClient.Setup(client => client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, mockMessageHandler.Object, mockDaprClient.Object); + + await receiver.SubscribeAsync(); + + //Write a message to the channel + var message = new TopicMessage("id", "source", "type", "specVersion", "dataContentType", topicName, pubSubName); + await receiver.WriteMessageToChannelAsync(message); + + //Allow some time for the message to be processed + await Task.Delay(100); + + mockMessageHandler.Verify(handler => handler(It.IsAny(), It.IsAny()), + Times.Once); + } + + [Fact] + public async Task SubscribeAsync_ShouldProcessAcknowledgements() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(30), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100 // Example value, adjust as needed + }; + + // Mock the message handler + var mockMessageHandler = new Mock(); + mockMessageHandler + .Setup(handler => handler(It.IsAny(), It.IsAny())) + .ReturnsAsync(TopicResponseAction.Success); + + // Mock the DaprClient + var mockDaprClient = new Mock(); + + // Create a mock AsyncDuplexStreamingCall + var mockRequestStream = new Mock>(); + var mockResponseStream = new Mock>(); + var mockCall = new AsyncDuplexStreamingCall( + mockRequestStream.Object, mockResponseStream.Object, Task.FromResult(new Metadata()), () => new Status(), () => new Metadata(), () => { }); + + // Setup the mock to return the mock call + mockDaprClient.Setup(client => client.SubscribeTopicEventsAlpha1(null, null, It.IsAny())) + .Returns(mockCall); + + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, mockMessageHandler.Object, mockDaprClient.Object); + + await receiver.SubscribeAsync(); + + // Use reflection to access the private acknowledgementsChannel and write an acknowledgement + var acknowledgementsChannelField = typeof(PublishSubscribeReceiver).GetField("acknowledgementsChannel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (acknowledgementsChannelField is null) + Assert.Fail(); + var acknowledgementsChannel = (Channel)acknowledgementsChannelField.GetValue(receiver)!; + + var acknowledgement = new PublishSubscribeReceiver.TopicAcknowledgement("id", TopicEventResponse.Types.TopicEventResponseStatus.Success); + await acknowledgementsChannel.Writer.WriteAsync(acknowledgement); + + // Allow some time for the acknowledgement to be processed + await Task.Delay(100); + + // Verify that the request stream's WriteAsync method was called twice (initial request + acknowledgement) + mockRequestStream.Verify(stream => stream.WriteAsync(It.IsAny(), It.IsAny()), Times.Exactly(2)); + } + + [Fact] + public async Task DisposeAsync_ShouldCompleteChannels() + { + const string pubSubName = "testPubSub"; + const string topicName = "testTopic"; + var options = + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(5), TopicResponseAction.Success)) + { + MaximumQueuedMessages = 100, MaximumCleanupTimeout = TimeSpan.FromSeconds(1) + }; + + var messageHandler = new TopicMessageHandler((message, topic) => Task.FromResult(TopicResponseAction.Success)); + var daprClient = new Mock(); + var receiver = new PublishSubscribeReceiver(pubSubName, topicName, options, messageHandler, daprClient.Object); + + await receiver.DisposeAsync(); + + Assert.True(receiver.TopicMessagesChannelCompletion.IsCompleted); + Assert.True(receiver.AcknowledgementsChannelCompletion.IsCompleted); + } + + [Fact] + public void HandleTaskCompletion_ShouldThrowException_WhenTaskHasException() + { + var task = Task.FromException(new InvalidOperationException("Test exception")); + + var exception = Assert.Throws(() => + PublishSubscribeReceiver.HandleTaskCompletion(task, null)); + + Assert.IsType(exception.InnerException); + Assert.Equal("Test exception", exception.InnerException.Message); + } +} From 3dd80196e7b1a3111d6e7ea199d942ad940b299a Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 12:49:49 -0800 Subject: [PATCH 53/69] Fix nulls Signed-off-by: Siri Varma Vegiraju --- .../ConversationalAI/Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ test/Dapr.Workflow.Test/WorkflowActivityTest.cs | 10 +++++++--- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 examples/AI/ConversationalAI/Properties/launchSettings.json create mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json diff --git a/examples/AI/ConversationalAI/Properties/launchSettings.json b/examples/AI/ConversationalAI/Properties/launchSettings.json new file mode 100644 index 000000000..040a51ff5 --- /dev/null +++ b/examples/AI/ConversationalAI/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ConversationalAI": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62602;http://localhost:62603" + } + } +} \ No newline at end of file diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json new file mode 100644 index 000000000..859475e5f --- /dev/null +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "StreamingSubscriptionExample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62604;http://localhost:62605" + } + } +} \ No newline at end of file diff --git a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs index 9fd96e60b..f6be41fc7 100644 --- a/test/Dapr.Workflow.Test/WorkflowActivityTest.cs +++ b/test/Dapr.Workflow.Test/WorkflowActivityTest.cs @@ -16,6 +16,7 @@ namespace Dapr.Workflow.Test using Moq; using System.Threading.Tasks; using Xunit; + using Xunit.Sdk; /// /// Contains tests for WorkflowActivityContext. @@ -26,15 +27,18 @@ public class WorkflowActivityTest private Mock workflowActivityContextMock; - [Fact] - public async Task RunAsync_ShouldReturnCorrectContextInstanceId() + public WorkflowActivityTest() { this.workflowActivity = new TestDaprWorkflowActivity(); this.workflowActivityContextMock = new Mock(); + } + [Fact] + public async Task RunAsync_ShouldReturnCorrectContextInstanceId() + { this.workflowActivityContextMock.Setup((x) => x.InstanceId).Returns("instanceId"); - string result = (string) await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"); + string result = (string) (await this.workflowActivity.RunAsync(this.workflowActivityContextMock.Object, "input"))!; Assert.Equal("instanceId", result); } From ce7b2d85565effe84fb22b1dbd8a6cafa510af56 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 14:55:46 -0800 Subject: [PATCH 54/69] Delete examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json Signed-off-by: Siri Varma Vegiraju --- .../Properties/launchSettings.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json deleted file mode 100644 index 859475e5f..000000000 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "StreamingSubscriptionExample": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:62604;http://localhost:62605" - } - } -} \ No newline at end of file From 63bad791de9316e0d1a27c3578274ca3e2e82f67 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 14:56:11 -0800 Subject: [PATCH 55/69] Delete examples/AI/ConversationalAI/Properties/launchSettings.json Signed-off-by: Siri Varma Vegiraju --- .../ConversationalAI/Properties/launchSettings.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 examples/AI/ConversationalAI/Properties/launchSettings.json diff --git a/examples/AI/ConversationalAI/Properties/launchSettings.json b/examples/AI/ConversationalAI/Properties/launchSettings.json deleted file mode 100644 index 040a51ff5..000000000 --- a/examples/AI/ConversationalAI/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "ConversationalAI": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:62602;http://localhost:62603" - } - } -} \ No newline at end of file From 57c93d2975c2f37527c34849148c687c6d553b79 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 14:57:07 -0800 Subject: [PATCH 56/69] Delete daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md Signed-off-by: Siri Varma Vegiraju --- .../en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md deleted file mode 100644 index 93700c383..000000000 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/dotnet-ai-usage.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -type: docs -title: "Best practices with the Dapr AI .NET SDK client" -linkTitle: "Best Practices" -weight: 100000 -description: How to get up and running with the Dapr .NET SDK ---- \ No newline at end of file From dd6b3c34ab16747164cfd4b787959fcf840277a3 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 14:58:42 -0800 Subject: [PATCH 57/69] Update dotnet-jobs-howto.md Signed-off-by: Siri Varma Vegiraju --- .../en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md index ad6021fce..0d63e9d5d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -64,11 +64,13 @@ the dependency injection registration in `Program.cs`, add the following line: var builder = WebApplication.CreateBuilder(args); //Add anywhere between these two +builder.Services.AddDaprJobsClient(); //That's it var app = builder.Build(); ``` > Note that in today's implementation of the Jobs API, the app that schedules the job will also be the app that receives the trigger notification. In other words, you cannot schedule a trigger to run in another application. As a result, while you don't explicitly need the Dapr Jobs client to be registered in your application to schedule a trigger invocation endpoint, your endpoint will never be invoked without the same app also scheduling the job somehow (whether via this Dapr Jobs .NET SDK or an HTTP call to the sidecar). + It's possible that you may want to provide some configuration options to the Dapr Jobs client that should be present with each call to the sidecar such as a Dapr API token, or you want to use a non-standard HTTP or gRPC endpoint. This is possible through use of an overload of the registration method that allows configuration of a @@ -367,4 +369,4 @@ public class MyOperation(DaprJobsClient daprJobsClient) await daprJobsClient.DeleteJobAsync(jobName, cancellationToken); } } -``` \ No newline at end of file +``` From e11bf0e271081a8a42075bc0acf3e09d6ad7d110 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 14:59:12 -0800 Subject: [PATCH 58/69] Update dotnet-jobs-howto.md Signed-off-by: Siri Varma Vegiraju From 0684a6c7113e589f13e585ee143b8bd12810202b Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 15:00:00 -0800 Subject: [PATCH 59/69] Update dotnet-workflowclient-usage.md Signed-off-by: Siri Varma Vegiraju --- .../dotnet-workflow/dotnet-workflowclient-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md index a0591373c..b014b5316 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -131,4 +131,4 @@ public class OrderProcessingWorkflow : Workflow //... } } -``` \ No newline at end of file +``` From 94763b334fecbb73a729067bda5c3f396cfe8b1d Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 15:00:25 -0800 Subject: [PATCH 60/69] Update dotnet-workflowclient-usage.md Signed-off-by: Siri Varma Vegiraju Signed-off-by: Siri Varma Vegiraju --- .../dotnet-workflow/dotnet-workflowclient-usage.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md index b014b5316..cc7ccabb9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -132,3 +132,6 @@ public class OrderProcessingWorkflow : Workflow } } ``` + + + From b519b6251696158b4f8635a8093fc81543161b03 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 15:02:02 -0800 Subject: [PATCH 61/69] fix thing Signed-off-by: Siri Varma Vegiraju --- .../content/en/dotnet-sdk-contributing/dotnet-contributing.md | 2 +- daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md | 2 +- daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md | 2 +- .../content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md | 2 +- .../dotnet-workflow/dotnet-workflowclient-usage.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md index d93334260..f0378b430 100644 --- a/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md +++ b/daprdocs/content/en/dotnet-sdk-contributing/dotnet-contributing.md @@ -114,4 +114,4 @@ As of v1.15, the following versions of .NET are supported: | .NET 9 | Will continue to be supported in v1.16 | Contributors are welcome to use whatever IDE they're most comfortable developing in, but please do not submit -IDE-specific preference files along with your contributions as these will be rejected. +IDE-specific preference files along with your contributions as these will be rejected. \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md index c21737e24..dac06e0bc 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-ai/_index.md @@ -9,4 +9,4 @@ description: Get up and running with the Dapr AI .NET SDK With the Dapr AI package, you can interact with the Dapr AI workloads from a .NET application. Today, Dapr provides the Conversational API to engage with large language models. To get started with this workload, -walk through the [Dapr Conversational AI]({{< ref dotnet-ai-conversation-howto.md >}}) how-to guide. +walk through the [Dapr Conversational AI]({{< ref dotnet-ai-conversation-howto.md >}}) how-to guide. \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md index 66461bd6c..60f756aa4 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md @@ -10,4 +10,4 @@ With the Dapr Job package, you can interact with the Dapr Job APIs from a .NET a to run according to a predefined schedule with an optional payload. To get started, walk through the [Dapr Jobs]({{< ref dotnet-jobs-howto.md >}}) how-to guide and refer to -[best practices documentation]({{< ref dotnet-jobs-usage.md >}}) for additional guidance. +[best practices documentation]({{< ref dotnet-jobs-usage.md >}}) for additional guidance. \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md index 0d63e9d5d..974b2f5ec 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -369,4 +369,4 @@ public class MyOperation(DaprJobsClient daprJobsClient) await daprJobsClient.DeleteJobAsync(jobName, cancellationToken); } } -``` +``` \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md index cc7ccabb9..a376e6acb 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -134,4 +134,4 @@ public class OrderProcessingWorkflow : Workflow ``` - + \ No newline at end of file From 8f27ba745126d8106c94d556f8296f721d75ea75 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 15:03:44 -0800 Subject: [PATCH 62/69] Update WorkflowActivityContext.cs Signed-off-by: Siri Varma Vegiraju Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Workflow/WorkflowActivityContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index a77c3ef91..1550b25cf 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -26,7 +26,7 @@ public abstract class WorkflowActivityContext public abstract TaskName Name { get; } /// - /// Gets the unique ID of the current workflow instance. + /// Gets the unique ID of current workflow instance. /// public abstract string InstanceId { get; } } From 096664af3814e58a8478f0d0c7c22e99d7fbb6fe Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 21:00:44 -0800 Subject: [PATCH 63/69] Update WorkflowActivityContext.cs Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Workflow/WorkflowActivityContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index 1550b25cf..3221cd039 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -26,7 +26,7 @@ public abstract class WorkflowActivityContext public abstract TaskName Name { get; } /// - /// Gets the unique ID of current workflow instance. + /// Gets the unique ID of thcurrent workflow instance. /// public abstract string InstanceId { get; } } From 1c4cc5b6a83ecc6b8750c863176378ec6968ef63 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 21:44:20 -0800 Subject: [PATCH 64/69] Fix version Signed-off-by: Siri Varma Vegiraju --- test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj index 066d7fcbc..26871d03c 100644 --- a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -13,6 +13,7 @@ + From e3e6b0a3c0f38deb80c7452b5cab8a8419a480e1 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 21:46:36 -0800 Subject: [PATCH 65/69] Update Dapr.Workflow.Test.csproj Signed-off-by: Siri Varma Vegiraju Signed-off-by: Siri Varma Vegiraju --- test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj index 26871d03c..36cee5ebc 100644 --- a/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj +++ b/test/Dapr.Workflow.Test/Dapr.Workflow.Test.csproj @@ -15,7 +15,6 @@ - From f32fb8e6b1ed5c33534bab02d9cda0cdfb82e2bb Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 22:44:44 -0800 Subject: [PATCH 66/69] fix things Signed-off-by: Siri Varma Vegiraju --- all.sln | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/all.sln b/all.sln index c03f6f909..9a163b1d9 100644 --- a/all.sln +++ b/all.sln @@ -113,9 +113,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject -<<<<<<< HEAD -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{7CA93D67-C551-430E-AA2C-BC64B77F7908}" -======= 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}" @@ -157,7 +154,6 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{9CAF360E-5AD3-4C4F-89A0-327EEB70D673}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" ->>>>>>> 096664af3814e58a8478f0d0c7c22e99d7fbb6fe EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -331,12 +327,6 @@ Global {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 -<<<<<<< HEAD - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7CA93D67-C551-430E-AA2C-BC64B77F7908}.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 @@ -413,7 +403,6 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU ->>>>>>> 096664af3814e58a8478f0d0c7c22e99d7fbb6fe EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -467,9 +456,6 @@ Global {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} -<<<<<<< HEAD - {7CA93D67-C551-430E-AA2C-BC64B77F7908} = {DD020B34-460F-455F-8D17-CF4A949F100B} -======= {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} @@ -491,7 +477,6 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} ->>>>>>> 096664af3814e58a8478f0d0c7c22e99d7fbb6fe EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} From b55699cbff4bc98755f765399e5215e3566231f9 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 22:43:50 -0800 Subject: [PATCH 67/69] Delete examples/AI/ConversationalAI/Properties/launchSettings.json Signed-off-by: Siri Varma Vegiraju Signed-off-by: Siri Varma Vegiraju --- .../ConversationalAI/Properties/launchSettings.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 examples/AI/ConversationalAI/Properties/launchSettings.json diff --git a/examples/AI/ConversationalAI/Properties/launchSettings.json b/examples/AI/ConversationalAI/Properties/launchSettings.json deleted file mode 100644 index a678d1823..000000000 --- a/examples/AI/ConversationalAI/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "ConversationalAI": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:59273;http://localhost:59274" - } - } -} \ No newline at end of file From ea4ac289352ac409dc531d46cfe5468b41e25574 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Fri, 20 Dec 2024 22:44:01 -0800 Subject: [PATCH 68/69] Delete examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json Signed-off-by: Siri Varma Vegiraju Signed-off-by: Siri Varma Vegiraju --- .../Properties/launchSettings.json | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json deleted file mode 100644 index fcd1bdf9b..000000000 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "StreamingSubscriptionExample": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:59271;http://localhost:59272" - } - } -} \ No newline at end of file From df7c70c0333e43db725c8266c37afd81aeaf6933 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Sat, 28 Dec 2024 05:27:24 -0800 Subject: [PATCH 69/69] Update WorkflowActivityContext.cs Signed-off-by: Siri Varma Vegiraju --- src/Dapr.Workflow/WorkflowActivityContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/WorkflowActivityContext.cs b/src/Dapr.Workflow/WorkflowActivityContext.cs index 3221cd039..a77c3ef91 100644 --- a/src/Dapr.Workflow/WorkflowActivityContext.cs +++ b/src/Dapr.Workflow/WorkflowActivityContext.cs @@ -26,7 +26,7 @@ public abstract class WorkflowActivityContext public abstract TaskName Name { get; } /// - /// Gets the unique ID of thcurrent workflow instance. + /// Gets the unique ID of the current workflow instance. /// public abstract string InstanceId { get; } }