From 610632ae4bf27890b66e9f970b5ae2a47c5c26d7 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Thu, 11 May 2023 13:11:49 -0700 Subject: [PATCH 01/84] Add WorkflowTaskFailedException for workflow error handling (#1086) Signed-off-by: Chris Gillum --- .../Activities/UpdateInventoryActivity.cs | 2 +- .../Workflows/OrderProcessingWorkflow.cs | 7 ++-- src/Dapr.Workflow/DaprWorkflowContext.cs | 34 ++++++++++++++-- src/Dapr.Workflow/WorkflowContext.cs | 8 ++-- .../WorkflowTaskFailedException.cs | 39 +++++++++++++++++++ 5 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 src/Dapr.Workflow/WorkflowTaskFailedException.cs diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index d136e74cc..5313c1f3b 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -39,7 +39,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay this.logger.LogInformation( "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", req.RequestId); - throw new InvalidOperationException(); + throw new InvalidOperationException("Not enough inventory!"); } // Update the statestore with the new amount of the item diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index faed132e3..4974cb997 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,5 +1,4 @@ using Dapr.Workflow; -using DurableTask.Core.Exceptions; using WorkflowConsoleApp.Activities; using WorkflowConsoleApp.Models; @@ -44,12 +43,12 @@ await context.CallActivityAsync( nameof(UpdateInventoryActivity), new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost)); } - catch (TaskFailedException) + catch (WorkflowTaskFailedException e) { - // Let them know their payment was processed + // Let them know their payment processing failed await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Order {orderId} Failed! You are now getting a refund")); + new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}")); return new OrderResult(Processed: false); } diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index d78b0e2df..3faa6c024 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -37,12 +37,12 @@ internal DaprWorkflowContext(TaskOrchestrationContext innerContext) public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) { - return this.innerContext.CallActivityAsync(name, input, options); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options)); } public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) { - return this.innerContext.CallActivityAsync(name, input, options); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options)); } public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) @@ -77,12 +77,12 @@ public override void SetCustomStatus(object? customStatus) public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) { - return this.innerContext.CallSubOrchestratorAsync(workflowName, input, options); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options)); } public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) { - return this.innerContext.CallSubOrchestratorAsync(workflowName, input, options); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options)); } public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) @@ -94,5 +94,31 @@ public override Guid NewGuid() { return this.innerContext.NewGuid(); } + + static async Task WrapExceptions(Task task) + { + try + { + await task; + } + catch (TaskFailedException ex) + { + var details = new WorkflowTaskFailureDetails(ex.FailureDetails); + throw new WorkflowTaskFailedException(ex.Message, details); + } + } + + static async Task WrapExceptions(Task task) + { + try + { + return await task; + } + catch (TaskFailedException ex) + { + var details = new WorkflowTaskFailureDetails(ex.FailureDetails); + throw new WorkflowTaskFailedException(ex.Message, details); + } + } } } diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index f6b7836b8..3928a2a7d 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -97,9 +97,9 @@ public abstract class WorkflowContext /// /// Thrown if the calling thread is not the workflow dispatch thread. /// - /// + /// /// The activity failed with an unhandled exception. The details of the failure can be found in the - /// property. + /// property. /// public virtual Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) { @@ -252,9 +252,9 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// Thrown if the calling thread is not the workflow dispatch thread. /// - /// + /// /// The child workflow failed with an unhandled exception. The details of the failure can be found in the - /// property. + /// property. /// public virtual Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) { diff --git a/src/Dapr.Workflow/WorkflowTaskFailedException.cs b/src/Dapr.Workflow/WorkflowTaskFailedException.cs new file mode 100644 index 000000000..575947a3a --- /dev/null +++ b/src/Dapr.Workflow/WorkflowTaskFailedException.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow +{ + using System; + + /// + /// Exception type for Dapr Workflow task failures. + /// + public class WorkflowTaskFailedException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The exception message. + /// Details about the failure. + public WorkflowTaskFailedException(string message, WorkflowTaskFailureDetails failureDetails) + : base(message) + { + this.FailureDetails = failureDetails ?? throw new ArgumentNullException(nameof(failureDetails)); + } + + /// + /// Gets more information about the underlying workflow task failure. + /// + public WorkflowTaskFailureDetails FailureDetails { get; } + } +} From 8152c7496a3e108d766969682061129de3eb7085 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 12 May 2023 15:44:33 -0700 Subject: [PATCH 02/84] [Workflow] Improve management API usability (#1087) * [Workflow] Improve management API usability Signed-off-by: Chris Gillum * PR feedback and update E2E test Signed-off-by: Chris Gillum * PR feedback Signed-off-by: Chris Gillum --------- Signed-off-by: Chris Gillum --- .editorconfig | 1 + .../Activities/UpdateInventoryActivity.cs | 2 +- .../Workflow/WorkflowConsoleApp/Program.cs | 28 +++--- src/Dapr.Client/DaprClient.cs | 81 ++++++++++++++- src/Dapr.Client/DaprClientGrpc.cs | 64 ++++++++---- src/Dapr.Client/GetWorkflowResponse.cs | 99 +++++++++++++++---- src/Dapr.Client/WorkflowFailureDetails.cs | 35 +++++++ .../WorkflowRuntimeStatus.cs | 4 +- src/Dapr.Workflow/DaprWorkflowClient.cs | 12 +-- src/Dapr.Workflow/WorkflowState.cs | 1 + test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 78 +++++++-------- 11 files changed, 304 insertions(+), 101 deletions(-) create mode 100644 src/Dapr.Client/WorkflowFailureDetails.cs rename src/{Dapr.Workflow => Dapr.Client}/WorkflowRuntimeStatus.cs (95%) diff --git a/.editorconfig b/.editorconfig index a4dd20cc2..2e2e40c7a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -22,6 +22,7 @@ charset = utf-8-bom # Organize usings dotnet_sort_system_directives_first = true dotnet_separate_import_directive_groups = false +csharp_using_directive_placement = outside_namespace # this. preferences dotnet_style_qualification_for_field = false:silent diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 5313c1f3b..aad48c183 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -39,7 +39,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay this.logger.LogInformation( "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", req.RequestId); - throw new InvalidOperationException("Not enough inventory!"); + throw new InvalidOperationException($"Not enough '{req.ItemName}' inventory! Requested {req.Amount} but only {item.Quantity} available."); } // Update the statestore with the new amount of the item diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index aa5e13f94..fef082ba5 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -4,9 +4,9 @@ using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.DependencyInjection; -const string storeName = "statestore"; +const string StoreName = "statestore"; +const string DaprWorkflowComponent = "dapr"; // The workflow host is a background service that connects to the sidecar over gRPC var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => @@ -58,8 +58,6 @@ // This is just to make the log output look a little nicer. Thread.Sleep(TimeSpan.FromSeconds(1)); -DaprWorkflowClient workflowClient = host.Services.GetRequiredService(); - var baseInventory = new List { new InventoryItem(Name: "Paperclips", PerItemCost: 5, Quantity: 100), @@ -114,15 +112,23 @@ // Start the workflow using the order ID as the workflow ID Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); - await workflowClient.ScheduleNewWorkflowAsync( - name: nameof(OrderProcessingWorkflow), + await daprClient.StartWorkflowAsync( + workflowComponent: DaprWorkflowComponent, + workflowName: nameof(OrderProcessingWorkflow), + input: orderInfo, + instanceId: orderId); + + // Wait for the workflow to start and confirm the input + GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( instanceId: orderId, - input: orderInfo); + workflowComponent: DaprWorkflowComponent); + + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); // Wait for the workflow to complete - WorkflowState state = await workflowClient.WaitForWorkflowCompletionAsync( + state = await daprClient.WaitForWorkflowCompletionAsync( instanceId: orderId, - getInputsAndOutputs: true); + workflowComponent: DaprWorkflowComponent); if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) { @@ -130,7 +136,7 @@ await workflowClient.ScheduleNewWorkflowAsync( if (result.Processed) { Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully."); + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); Console.ResetColor(); } else @@ -154,6 +160,6 @@ static async Task RestockInventory(DaprClient daprClient, List in foreach (var item in inventory) { Console.WriteLine($"*** \t{item.Name}: {item.Quantity}"); - await daprClient.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item); + await daprClient.SaveStateAsync(StoreName, item.Name.ToLowerInvariant(), item); } } diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 56b16c459..ffc6058c5 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -978,22 +978,95 @@ public abstract Task Unlock( /// /// Attempt to start the given workflow with response indicating success. /// - /// Identifier of the specific run. /// 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. - /// The input input for the given workflow. /// A that can be used to cancel the operation. /// A containing a [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task StartWorkflowAsync( - string instanceId, string workflowComponent, string workflowName, - Object input, + 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. + /// + [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. + /// + /// + [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. /// diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 61a73d920..a3bfdd340 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -1467,10 +1467,10 @@ public async override Task Unlock( /// [Obsolete] public async override Task StartWorkflowAsync( - string instanceId, string workflowComponent, string workflowName, - Object input, + string instanceId = null, + object input = null, IReadOnlyDictionary workflowOptions = default, CancellationToken cancellationToken = default) { @@ -1480,14 +1480,18 @@ public async override Task StartWorkflowAsync( ArgumentVerifier.ThrowIfNull(input, nameof(input)); // Serialize json data. Converts input object to bytes and then bytestring inside the request. - var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); + byte[] jsonUtf8Bytes = null; + if (input is not null) + { + jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(input); + } var request = new Autogenerated.StartWorkflowRequest() { InstanceId = instanceId, WorkflowComponent = workflowComponent, WorkflowName = workflowName, - Input = ByteString.CopyFrom(jsonUtf8Bytes), + Input = jsonUtf8Bytes is not null ? ByteString.CopyFrom(jsonUtf8Bytes) : null, }; if (workflowOptions?.Count > 0) @@ -1533,31 +1537,53 @@ public async override Task GetWorkflowAsync( var response = await client.GetWorkflowAlpha1Async(request, options); if (response == null) { - throw new DaprException("Get workflow operation failed: the object response is null"); - } - if (response.CreatedAt == null) - { - response.CreatedAt = new Timestamp(); - throw new DaprException("Get workflow operation failed: CreatedAt object response is null"); + throw new DaprException("Get workflow operation failed: the Dapr endpoint returned an empty result."); } - if (response.LastUpdatedAt == null) + + response.CreatedAt ??= new Timestamp(); + response.LastUpdatedAt ??= response.CreatedAt; + + return new GetWorkflowResponse { - response.LastUpdatedAt = response.CreatedAt; - } - return new GetWorkflowResponse(response.InstanceId, - response.WorkflowName, - response.CreatedAt.ToDateTime(), - response.LastUpdatedAt.ToDateTime(), - response.RuntimeStatus, - response.Properties); + 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] diff --git a/src/Dapr.Client/GetWorkflowResponse.cs b/src/Dapr.Client/GetWorkflowResponse.cs index eb24b9429..11fc253ac 100644 --- a/src/Dapr.Client/GetWorkflowResponse.cs +++ b/src/Dapr.Client/GetWorkflowResponse.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. @@ -10,26 +10,91 @@ // 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; } + /// - /// Initializes a new . - /// - /// The instance ID associated with this response. - /// The name of the workflow associated with this response. - /// The time at which the workflow started executing. - /// The time at which the workflow started executing. - /// The current runtime status of the workflow. - /// The response properties. - public record GetWorkflowResponse( - string InstanceId, - string WorkflowName, - DateTime CreatedAt, - DateTime LastUpdatedAt, - string RuntimeStatus, - IReadOnlyDictionary Properties); + /// 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 new file mode 100644 index 000000000..a61754ff1 --- /dev/null +++ b/src/Dapr.Client/WorkflowFailureDetails.cs @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------ +// 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.Workflow/WorkflowRuntimeStatus.cs b/src/Dapr.Client/WorkflowRuntimeStatus.cs similarity index 95% rename from src/Dapr.Workflow/WorkflowRuntimeStatus.cs rename to src/Dapr.Client/WorkflowRuntimeStatus.cs index 01198d327..dc652630e 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeStatus.cs +++ b/src/Dapr.Client/WorkflowRuntimeStatus.cs @@ -11,10 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Workflow +namespace Dapr.Client { /// - /// Enum describing the runtime status of the workflow. + /// Enum describing the runtime status of a workflow. /// public enum WorkflowRuntimeStatus { diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index e50632be5..249de09b7 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -10,15 +10,15 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ +using System; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; namespace Dapr.Workflow { - using System; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.DurableTask; - using Microsoft.DurableTask.Client; - /// /// Defines client operations for managing Dapr Workflow instances. /// diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index 7be914133..aefe4be6b 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -14,6 +14,7 @@ namespace Dapr.Workflow { using System; + using Dapr.Client; using Microsoft.DurableTask.Client; /// diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index 5ba9f283e..ae30a9151 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // 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. @@ -10,20 +10,17 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Client; +using FluentAssertions; +using Xunit; namespace Dapr.E2E.Test { - - using System.Threading; - using System.Threading.Tasks; - using Xunit; - using FluentAssertions; - using System; - using System.Collections.Generic; - using Google.Protobuf; - using Dapr.Client; - - [System.Obsolete] + [Obsolete] public partial class E2ETests { [Fact] @@ -36,7 +33,6 @@ public async Task TestWorkflows() object input = "paperclips"; Dictionary workflowOptions = new Dictionary(); workflowOptions.Add("task_queue", "testQueue"); - CancellationToken cts = new CancellationToken(); using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); var health = await daprClient.CheckHealthAsync(); @@ -48,43 +44,41 @@ public async Task TestWorkflows() workflowComponent: workflowComponent, workflowName: workflowName, input: input, - workflowOptions: workflowOptions, - cancellationToken: cts); + workflowOptions: workflowOptions); startResponse.InstanceId.Should().Be("testInstanceId", $"Instance ID {startResponse.InstanceId} was not correct"); - // GET INFO TEST - var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); + var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); getResponse.InstanceId.Should().Be("testInstanceId"); - getResponse.RuntimeStatus.Should().Be("RUNNING", $"Instance ID {getResponse.RuntimeStatus} was not correct"); + getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); // PAUSE TEST: - await daprClient.PauseWorkflowAsync(instanceId, workflowComponent, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("SUSPENDED", $"Instance ID {getResponse.RuntimeStatus} was not correct"); + 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, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("RUNNING", $"Instance ID {getResponse.RuntimeStatus} was not correct"); + 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", cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); + await daprClient.RaiseWorkflowEventAsync(instanceId, workflowComponent, "ChangePurchaseItem", "computers"); + getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); // TERMINATE TEST: - await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent, cts); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - getResponse.RuntimeStatus.Should().Be("TERMINATED", $"Instance ID {getResponse.RuntimeStatus} was not correct"); + 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, cts); + await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent); try { - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, cts); - Assert.True(false); + getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); + Assert.True(false, "The GetWorkflowAsync call should have failed since the instance was purged"); } catch (DaprException ex) { @@ -92,21 +86,23 @@ public async Task TestWorkflows() } // Start another workflow for event raising purposes - startResponse = await daprClient.StartWorkflowAsync(instanceId: instanceId2, + startResponse = await daprClient.StartWorkflowAsync( + instanceId: instanceId2, workflowComponent: workflowComponent, workflowName: workflowName, input: input, - workflowOptions: workflowOptions, - cancellationToken: cts); + workflowOptions: workflowOptions); // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers", cts); - await Task.Delay(TimeSpan.FromSeconds(30)); - getResponse = await daprClient.GetWorkflowAsync(instanceId2, workflowComponent, cts); + await daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); + + // 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"); } - } -} \ No newline at end of file +} From a2d3c3a48c0ff1c7226f0f4c5da2168d33de5e36 Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Thu, 18 May 2023 10:25:21 -0400 Subject: [PATCH 03/84] [docs] Update API calls in workflow example (#1083) * update calls in the example Signed-off-by: Hannah Hunter * remove gitmodules Signed-off-by: Hannah Hunter * remove daprdocs/themes/docsy Signed-off-by: Hannah Hunter * fix links and add links Signed-off-by: Hannah Hunter * Fix workflow inputs and outputs for v1.11 --------- Signed-off-by: Hannah Hunter Co-authored-by: Chris Gillum --- .../dotnet-workflow/dotnet-workflow-howto.md | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 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 d19aee6dd..c1c792bc5 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md @@ -8,13 +8,13 @@ description: Learn how to author and manage Dapr Workflow using the .NET SDK Let's create a Dapr workflow and invoke it using the console. In the [provided order processing workflow example](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow), the console prompts provide directions on how to both purchase and restock items. In this guide, you will: -- Create a .NET console application ([WorkflowConsoleApp](./WorkflowConsoleApp)). +- Deploy a .NET console application ([WorkflowConsoleApp](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp)). - Utilize the .NET workflow SDK and API calls to start and query workflow instances. In the .NET example project: -- The main `Program.cs` file contains the setup of the app, including the registration of the workflow and workflow activities. -- The workflow definition is found in the `Workflows` directory. -- The workflow activity definitions are found in the `Activities` directory. +- The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/blob/master/examples/Workflow/WorkflowConsoleApp/Program.cs) file contains the setup of the app, including the registration of the workflow and workflow activities. +- The workflow definition is found in the [`Workflows` directory](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp/Workflows). +- The workflow activity definitions are found in the [`Activities` directory](https://github.com/dapr/dotnet-sdk/tree/master/examples/Workflow/WorkflowConsoleApp/Activities). ## Prerequisites @@ -72,7 +72,7 @@ This guide focuses on the workflow API option. {{% alert title="Note" color="primary" %}} - You can find the commands below in the `WorkflowConsoleApp`/`demo.http` file. - The body of the curl request is the purchase order information used as the input of the workflow. - - The "1234" in the commands represents the unique identifier for the workflow and can be replaced with any identifier of your choosing. + - The "12345678" in the commands represents the unique identifier for the workflow and can be replaced with any identifier of your choosing. {{% /alert %}} @@ -83,9 +83,9 @@ Run the following command to start a workflow. {{% codetab %}} ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start \ +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ -H "Content-Type: application/json" \ - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` {{% /codetab %}} @@ -93,9 +93,9 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing {{% codetab %}} ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start ` +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` -H "Content-Type: application/json" ` - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` {{% /codetab %}} @@ -105,30 +105,27 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing If successful, you should see a response like the following: ```json -{"instance_id":"1234"} +{"instanceID":"12345678"} ``` Send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 +curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/12345678 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed when you issue the HTTP request, you'll see the following JSON response (formatted for readability) with workflow status as `RUNNING`: ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "12345678", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:06.142214153Z", + "runtimeStatus": "RUNNING", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "RUNNING" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" } } ``` @@ -137,17 +134,15 @@ Once the workflow has completed running, you should see the following output, in ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "12345678", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:18.527704176Z", + "runtimeStatus": "COMPLETED", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "COMPLETED" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}", + "dapr.workflow.output": "{\"Processed\":true}" } } ``` @@ -156,13 +151,13 @@ When the workflow has completed, the stdout of the workflow app should look like ```log info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Received order 1234 for Paperclips at $99.95 + Received order 12345678 for Paperclips at $99.95 info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0] - Reserving inventory: 1234, Paperclips, 1 + Reserving inventory: 12345678, Paperclips, 1 info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0] - Processing payment: 1234, 99.95, USD + Processing payment: 12345678, 99.95, USD info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Order 1234 processed successfully! + Order 12345678 processed successfully! ``` If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). From e59c856b335db78274a3012d4a1c13f2c4038920 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 19 May 2023 07:22:13 -0700 Subject: [PATCH 04/84] Proper workflow retry support in Dapr SDK (#1090) Signed-off-by: Chris Gillum Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .../Activities/ReserveInventoryActivity.cs | 5 +- .../Activities/UpdateInventoryActivity.cs | 3 +- .../Workflows/OrderProcessingWorkflow.cs | 18 ++- .../WorkflowUnitTest/OrderProcessingTests.cs | 17 ++- src/Dapr.Workflow/DaprWorkflowContext.cs | 16 +-- src/Dapr.Workflow/WorkflowContext.cs | 27 +++-- src/Dapr.Workflow/WorkflowRetryPolicy.cs | 104 ++++++++++++++++++ src/Dapr.Workflow/WorkflowTaskOptions.cs | 68 ++++++++++++ 8 files changed, 221 insertions(+), 37 deletions(-) create mode 100644 src/Dapr.Workflow/WorkflowRetryPolicy.cs create mode 100644 src/Dapr.Workflow/WorkflowTaskOptions.cs diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 7dce1f46a..fc6c48921 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Dapr.Client; +using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; using WorkflowConsoleApp.Models; @@ -27,7 +26,7 @@ public override async Task RunAsync(WorkflowActivityContext con req.ItemName); // Ensure that the store has items - InventoryItem item = await client.GetStateAsync( + InventoryItem item = await this.client.GetStateAsync( storeName, req.ItemName.ToLowerInvariant()); diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index aad48c183..947dab6cb 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Dapr.Client; +using Dapr.Client; using Dapr.Workflow; using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 4974cb997..bedd77f2a 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -6,11 +6,18 @@ namespace WorkflowConsoleApp.Workflows { public class OrderProcessingWorkflow : Workflow { + readonly WorkflowTaskOptions defaultActivityRetryOptions = new WorkflowTaskOptions + { + // NOTE: Beware that changing the number of retries is a breaking change for existing workflows. + RetryPolicy = new WorkflowRetryPolicy( + maxNumberOfAttempts: 3, + firstRetryInterval: TimeSpan.FromSeconds(5)), + }; + public override async Task RunAsync(WorkflowContext context, OrderPayload order) { string orderId = context.InstanceId; - // Notify the user that an order has come through await context.CallActivityAsync( nameof(NotifyActivity), @@ -19,7 +26,8 @@ await context.CallActivityAsync( // Determine if there is enough of the item available for purchase by checking the inventory InventoryResult result = await context.CallActivityAsync( nameof(ReserveInventoryActivity), - new InventoryRequest(RequestId: orderId, order.Name, order.Quantity)); + new InventoryRequest(RequestId: orderId, order.Name, order.Quantity), + this.defaultActivityRetryOptions); // If there is insufficient inventory, fail and let the user know if (!result.Success) @@ -34,14 +42,16 @@ await context.CallActivityAsync( // There is enough inventory available so the user can purchase the item(s). Process their payment await context.CallActivityAsync( nameof(ProcessPaymentActivity), - new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost)); + new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), + this.defaultActivityRetryOptions); try { // There is enough inventory available so the user can purchase the item(s). Process their payment await context.CallActivityAsync( nameof(UpdateInventoryActivity), - new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost)); + new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), + this.defaultActivityRetryOptions); } catch (WorkflowTaskFailedException e) { diff --git a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs index 0cb450fcd..ac53c4081 100644 --- a/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs +++ b/examples/Workflow/WorkflowUnitTest/OrderProcessingTests.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Dapr.Workflow; -using Microsoft.DurableTask; using Moq; using WorkflowConsoleApp.Activities; using WorkflowConsoleApp.Models; @@ -24,7 +23,7 @@ public async Task TestSuccessfulOrder() // Mock the call to ReserveInventoryActivity Mock mockContext = new(); mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly @@ -36,17 +35,17 @@ public async Task TestSuccessfulOrder() // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), Times.Once()); // Verify that ProcessPaymentActivity was called with a specific input mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), expectedPaymentRequest, It.IsAny()), Times.Once()); // Verify that there were two calls to NotifyActivity mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), Times.Exactly(2)); } @@ -61,7 +60,7 @@ public async Task TestInsufficientInventory() // Mock the call to ReserveInventoryActivity Mock mockContext = new(); mockContext - .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) + .Setup(ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), It.IsAny(), It.IsAny())) .Returns(Task.FromResult(inventoryResult)); // Run the workflow directly @@ -69,17 +68,17 @@ public async Task TestInsufficientInventory() // Verify that ReserveInventoryActivity was called with a specific input mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(ReserveInventoryActivity), expectedInventoryRequest, It.IsAny()), Times.Once()); // Verify that ProcessPaymentActivity was never called mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny(), It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(ProcessPaymentActivity), It.IsAny(), It.IsAny()), Times.Never()); // Verify that there were two calls to NotifyActivity mockContext.Verify( - ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), + ctx => ctx.CallActivityAsync(nameof(NotifyActivity), It.IsAny(), It.IsAny()), Times.Exactly(2)); } } diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index 3faa6c024..be08ef421 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -35,14 +35,14 @@ internal DaprWorkflowContext(TaskOrchestrationContext innerContext) public override bool IsReplaying => this.innerContext.IsReplaying; - public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { - return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options)); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); } - public override Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { - return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options)); + return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); } public override Task CreateTimer(TimeSpan delay, CancellationToken cancellationToken = default) @@ -75,14 +75,14 @@ public override void SetCustomStatus(object? customStatus) this.innerContext.SetCustomStatus(customStatus); } - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) { - return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options)); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); } - public override Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public override Task CallChildWorkflowAsync(string workflowName, object? input = null, ChildWorkflowTaskOptions? options = null) { - return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options)); + return WrapExceptions(this.innerContext.CallSubOrchestratorAsync(workflowName, input, options?.ToDurableTaskOptions())); } public override void ContinueAsNew(object? newInput = null, bool preserveUnprocessedEvents = true) diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index 3928a2a7d..c7436696c 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -16,7 +16,6 @@ namespace Dapr.Workflow using System; using System.Threading; using System.Threading.Tasks; - using Microsoft.DurableTask; /// /// Context object used by workflow implementations to perform actions such as scheduling activities, durable timers, waiting for @@ -101,7 +100,7 @@ public abstract class WorkflowContext /// The activity failed with an unhandled exception. The details of the failure can be found in the /// property. /// - public virtual Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null) + public virtual Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { return this.CallActivityAsync(name, input, options); } @@ -110,7 +109,7 @@ public virtual Task CallActivityAsync(string name, object? input = null, TaskOpt /// A task that completes when the activity completes or fails. The result of the task is the activity's return value. /// /// - public abstract Task CallActivityAsync(string name, object? input = null, TaskOptions? options = null); + public abstract Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null); /// /// Creates a durable timer that expires after the specified delay. @@ -212,8 +211,11 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// The type into which to deserialize the child workflow's output. /// - /// - public abstract Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null); + /// + public abstract Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null); /// /// Executes the specified workflow as a child workflow. @@ -222,7 +224,8 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// In addition to activities, workflows can schedule other workflows as child workflows. /// A child workflow has its own instance ID, history, and status that is independent of the parent workflow - /// that started it. + /// that started it. You can use to specify an instance ID + /// for the child workflow. Otherwise, the instance ID will be randomly generated. /// /// Child workflows have many benefits: /// @@ -237,15 +240,14 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// exception. Child workflows also support automatic retry policies. /// /// Because child workflows are independent of their parents, terminating a parent workflow does not affect - /// any child workflows. You must terminate each child workflow independently using its instance ID, which is - /// specified by supplying in place of . + /// any child workflows. You must terminate each child workflow independently using its instance ID, which + /// is specified by . /// /// /// The name of the workflow to call. /// The serializable input to pass to the child workflow. /// - /// Additional options that control the execution and processing of the child workflow. Callers can choose to - /// supply the derived type . + /// Additional options that control the execution and processing of the child workflow. /// /// A task that completes when the child workflow completes or fails. /// The specified workflow does not exist. @@ -256,7 +258,10 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// The child workflow failed with an unhandled exception. The details of the failure can be found in the /// property. /// - public virtual Task CallChildWorkflowAsync(string workflowName, object? input = null, TaskOptions? options = null) + public virtual Task CallChildWorkflowAsync( + string workflowName, + object? input = null, + ChildWorkflowTaskOptions? options = null) { return this.CallChildWorkflowAsync(workflowName, input, options); } diff --git a/src/Dapr.Workflow/WorkflowRetryPolicy.cs b/src/Dapr.Workflow/WorkflowRetryPolicy.cs new file mode 100644 index 000000000..35f0f9108 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowRetryPolicy.cs @@ -0,0 +1,104 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +using System; +using System.Threading; +using Microsoft.DurableTask; + +namespace Dapr.Workflow +{ + /// + /// A declarative retry policy that can be configured for activity or child workflow calls. + /// + public class WorkflowRetryPolicy + { + readonly RetryPolicy durableRetryPolicy; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of task invocation attempts. Must be 1 or greater. + /// The amount of time to delay between the first and second attempt. + /// + /// The exponential back-off coefficient used to determine the delay between subsequent retries. Must be 1.0 or greater. + /// + /// + /// The maximum time to delay between attempts, regardless of. + /// + /// The overall timeout for retries. + /// + /// The value can be used to specify an unlimited timeout for + /// or . + /// + /// + /// Thrown if any of the following are true: + /// + /// The value for is less than or equal to zero. + /// The value for is less than or equal to . + /// The value for is less than 1.0. + /// The value for is less than . + /// The value for is less than . + /// + /// + public WorkflowRetryPolicy( + int maxNumberOfAttempts, + TimeSpan firstRetryInterval, + double backoffCoefficient = 1.0, + TimeSpan? maxRetryInterval = null, + TimeSpan? retryTimeout = null) + { + this.durableRetryPolicy = new RetryPolicy( + maxNumberOfAttempts, + firstRetryInterval, + backoffCoefficient, + maxRetryInterval, + retryTimeout); + } + + /// + /// Gets the max number of attempts for executing a given task. + /// + public int MaxNumberOfAttempts => this.durableRetryPolicy.MaxNumberOfAttempts; + + /// + /// Gets the amount of time to delay between the first and second attempt. + /// + public TimeSpan FirstRetryInterval => this.durableRetryPolicy.FirstRetryInterval; + + /// + /// Gets the exponential back-off coefficient used to determine the delay between subsequent retries. + /// + /// + /// Defaults to 1.0 for no back-off. + /// + public double BackoffCoefficient => this.durableRetryPolicy.BackoffCoefficient; + + /// + /// Gets the maximum time to delay between attempts. + /// + /// + /// Defaults to 1 hour. + /// + public TimeSpan MaxRetryInterval => this.durableRetryPolicy.MaxRetryInterval; + + /// + /// Gets the overall timeout for retries. No further attempts will be made at executing a task after this retry + /// timeout expires. + /// + /// + /// Defaults to . + /// + public TimeSpan RetryTimeout => this.durableRetryPolicy.RetryTimeout; + + internal RetryPolicy GetDurableRetryPolicy() => this.durableRetryPolicy; + } +} diff --git a/src/Dapr.Workflow/WorkflowTaskOptions.cs b/src/Dapr.Workflow/WorkflowTaskOptions.cs new file mode 100644 index 000000000..d93dbc551 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowTaskOptions.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.DurableTask; + +namespace Dapr.Workflow +{ + /// + /// Options that can be used to control the behavior of workflow task execution. + /// + /// The workflow retry policy. + public record WorkflowTaskOptions(WorkflowRetryPolicy? RetryPolicy = null) + { + internal TaskOptions ToDurableTaskOptions() + { + TaskRetryOptions? retryOptions = null; + if (this.RetryPolicy is not null) + { + retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); + } + + return new TaskOptions(retryOptions); + } + } + + /// + /// Options for controlling the behavior of child workflow execution. + /// + public record ChildWorkflowTaskOptions : WorkflowTaskOptions + { + /// + /// Initializes a new instance of the record. + /// + /// The instance ID to use for the child workflow. + /// The child workflow's retry policy. + public ChildWorkflowTaskOptions(string? instanceId = null, WorkflowRetryPolicy ? retryPolicy = null) + : base(retryPolicy) + { + this.InstanceId = instanceId; + } + + /// + /// Gets the instance ID to use when creating a child workflow. + /// + public string? InstanceId { get; init; } + + internal new SubOrchestrationOptions ToDurableTaskOptions() + { + TaskRetryOptions? retryOptions = null; + if (this.RetryPolicy is not null) + { + retryOptions = this.RetryPolicy.GetDurableRetryPolicy(); + } + + return new SubOrchestrationOptions(retryOptions, this.InstanceId); + } + } +} From 364ed92f958a7bb85746719a5cc7b103ea2c2c38 Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Mon, 22 May 2023 13:54:35 -0400 Subject: [PATCH 05/84] [dotnet-client] Add dist lock examples (#1095) * add dist lock to dotnet client doc Signed-off-by: Hannah Hunter * attempt at naming scheme 1 Signed-off-by: Hannah Hunter * yikes put it back Signed-off-by: Hannah Hunter * attempt 2 Signed-off-by: Hannah Hunter * attempt 3 Signed-off-by: Hannah Hunter --------- Signed-off-by: Hannah Hunter --- .../dotnet-sdk-docs/dotnet-client/_index.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index 0404857e8..beeb76f70 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -199,6 +199,64 @@ await foreach (var items in subscribeConfigurationResponse.Source.WithCancellati } ``` +### Distributed lock (Alpha) + +#### Acquire a lock + +```csharp +using System; +using Dapr.Client; + +namespace LockService +{ + class Program + { + [Obsolete("Distributed Lock API is in Alpha, this can be removed once it is stable.")] + static async Task Main(string[] args) + { + var daprLockName = "lockstore"; + var fileName = "my_file_name"; + var client = new DaprClientBuilder().Build(); + + // Locking with this approach will also unlock it automatically, as this is a disposable object + await using (var fileLock = await client.Lock(DAPR_LOCK_NAME, fileName, "random_id_abc123", 60)) + { + if (fileLock.Success) + { + Console.WriteLine("Success"); + } + else + { + Console.WriteLine($"Failed to lock {fileName}."); + } + } + } + } +} +``` + +#### Unlock an existing lock + +```csharp +using System; +using Dapr.Client; + +namespace LockService +{ + class Program + { + static async Task Main(string[] args) + { + var daprLockName = "lockstore"; + var client = new DaprClientBuilder().Build(); + + var response = await client.Unlock(DAPR_LOCK_NAME, "my_file_name", "random_id_abc123")); + Console.WriteLine(response.status); + } + } +} +``` + ### Manage workflow instances (Alpha) ```csharp From a3e5106040453bada9433825c884ab8815f63408 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Wed, 24 May 2023 10:43:54 -0700 Subject: [PATCH 06/84] [Workflow] Add human approval to the workflow example (#1096) * [Workflow] Add human approval to the workflow example Signed-off-by: Chris Gillum * Add RequestApprovalActivity to workflow Signed-off-by: Chris Gillum * PR feedback - explicit enum values Signed-off-by: Chris Gillum --------- Signed-off-by: Chris Gillum --- examples/Workflow/README.md | 47 ++++++++--------- .../Activities/RequestApprovalActivity.cs | 24 +++++++++ .../Workflow/WorkflowConsoleApp/Models.cs | 6 +++ .../Workflow/WorkflowConsoleApp/Program.cs | 52 +++++++++++++++++-- .../Workflows/OrderProcessingWorkflow.cs | 33 ++++++++++++ src/Dapr.Client/DaprClient.cs | 9 ++++ src/Dapr.Workflow/WorkflowContext.cs | 6 +++ 7 files changed, 148 insertions(+), 29 deletions(-) create mode 100644 examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 610077d47..b93af1bb5 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -49,46 +49,43 @@ For the workflow API option, two identical `curl` commands are shown, one for Li Make note of the "1234" in the commands below. This represents the unique identifier for the workflow run and can be replaced with any identifier of your choosing. ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start \ +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 \ -H "Content-Type: application/json" \ - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` On Windows (PowerShell): ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start ` +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 ` -H "Content-Type: application/json" ` - -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' + -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` If successful, you should see a response like the following: ```json -{"instance_id":"1234"} +{"instanceID":"1234"} ``` Next, send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 +curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed yet when you issue the previous command, you should see the following JSON response (formatted for readability): ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "1234", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:06.142214153Z", + "runtimeStatus": "RUNNING", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "RUNNING" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}" } } ``` @@ -97,17 +94,15 @@ Once the workflow has completed running, you should see the following output, in ```json { - "WFInfo": { - "instance_id": "1234" - }, - "start_time": "2023-02-02T23:34:53Z", - "metadata": { + "instanceID": "1234", + "workflowName": "OrderProcessingWorkflow", + "createdAt": "2023-05-10T00:42:03.911444105Z", + "lastUpdatedAt": "2023-05-10T00:42:18.527704176Z", + "runtimeStatus": "COMPLETED", + "properties": { "dapr.workflow.custom_status": "", - "dapr.workflow.input": "{\"Name\":\"Paperclips\",\"Quantity\":1,\"TotalCost\":99.95}", - "dapr.workflow.last_updated": "2023-02-02T23:35:07Z", - "dapr.workflow.name": "OrderProcessingWorkflow", - "dapr.workflow.output": "{\"Processed\":true}", - "dapr.workflow.runtime_status": "COMPLETED" + "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"TotalCost\": 99.95, \"Quantity\": 1}", + "dapr.workflow.output": "{\"Processed\":true}" } } ``` diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs new file mode 100644 index 000000000..af0b1fa13 --- /dev/null +++ b/examples/Workflow/WorkflowConsoleApp/Activities/RequestApprovalActivity.cs @@ -0,0 +1,24 @@ +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +using WorkflowConsoleApp.Models; + +namespace WorkflowConsoleApp.Activities +{ + public class RequestApprovalActivity : WorkflowActivity + { + readonly ILogger logger; + + public RequestApprovalActivity(ILoggerFactory loggerFactory) + { + this.logger = loggerFactory.CreateLogger(); + } + + public override Task RunAsync(WorkflowActivityContext context, OrderPayload input) + { + string orderId = context.InstanceId.ToString(); + this.logger.LogInformation("Requesting approval for order {orderId}", orderId); + + return Task.FromResult(null); + } + } +} diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index f2f3dcbc8..6c9583d84 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -6,4 +6,10 @@ public record InventoryResult(bool Success, InventoryItem orderPayload); public record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency); public record OrderResult(bool Processed); public record InventoryItem(string Name, double PerItemCost, int Quantity); + public enum ApprovalResult + { + Unspecified = 0, + Approved = 1, + Rejected = 2, + } } diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index fef082ba5..a1189e70b 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -20,6 +20,7 @@ // These are the activities that get invoked by the workflow(s). options.RegisterActivity(); options.RegisterActivity(); + options.RegisterActivity(); options.RegisterActivity(); options.RegisterActivity(); }); @@ -126,9 +127,54 @@ await daprClient.StartWorkflowAsync( Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); // Wait for the workflow to complete - state = await daprClient.WaitForWorkflowCompletionAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); + while (true) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try + { + state = await daprClient.WaitForWorkflowCompletionAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + cancellationToken: cts.Token); + break; + } + catch (OperationCanceledException) + { + // Check to see if the workflow is blocked waiting for an approval + state = await daprClient.GetWorkflowAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent); + if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && + customStatus.Contains("Waiting for approval")) + { + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); + string approval = Console.ReadLine(); + ApprovalResult approvalResult = ApprovalResult.Unspecified; + if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Approving order..."); + approvalResult = ApprovalResult.Approved; + } + else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Rejecting order..."); + approvalResult = ApprovalResult.Rejected; + } + + if (approvalResult != ApprovalResult.Unspecified) + { + // Raise the workflow event to the workflow + await daprClient.RaiseWorkflowEventAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + eventName: "ManagerApproval", + eventData: approvalResult); + } + + // otherwise, keep waiting + } + } + } if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) { diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index bedd77f2a..bd2a710b6 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -39,6 +39,39 @@ await context.CallActivityAsync( return new OrderResult(Processed: false); } + // Require orders over a certain threshold to be approved + if (order.TotalCost > 50000) + { + // Request manager approval for the order + await context.CallActivityAsync(nameof(RequestApprovalActivity), order); + + try + { + // Pause and wait for a manager to approve the order + context.SetCustomStatus("Waiting for approval"); + ApprovalResult approvalResult = await context.WaitForExternalEventAsync( + eventName: "ManagerApproval", + timeout: TimeSpan.FromSeconds(30)); + context.SetCustomStatus($"Approval result: {approvalResult}"); + if (approvalResult == ApprovalResult.Rejected) + { + // The order was rejected, end the workflow here + await context.CallActivityAsync( + nameof(NotifyActivity), + new Notification($"Order was rejected by approver")); + return new OrderResult(Processed: false); + } + } + catch (TaskCanceledException) + { + // An approval timeout results in automatic order cancellation + await context.CallActivityAsync( + nameof(NotifyActivity), + new Notification($"Cancelling order because it didn't receive an approval")); + return new OrderResult(Processed: false); + } + } + // There is enough inventory available so the user can purchase the item(s). Process their payment await context.CallActivityAsync( nameof(ProcessPaymentActivity), diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index ffc6058c5..de0813eb8 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -1011,6 +1011,9 @@ public abstract Task StartWorkflowAsync( /// /// 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, @@ -1047,6 +1050,12 @@ public virtual async Task WaitForWorkflowStartAsync( /// 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, diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index c7436696c..f4e500134 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -159,6 +159,9 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// /// A task that completes when the external event is received. The value of the task is the deserialized event payload. /// + /// + /// Thrown if is cancelled before the external event is received. + /// /// /// Thrown if the calling thread is not the workflow dispatch thread. /// @@ -172,6 +175,9 @@ public virtual Task CreateTimer(TimeSpan delay, CancellationToken cancellationTo /// number of times; they are not required to be unique. /// /// The amount of time to wait before cancelling the external event task. + /// + /// Thrown if elapses before the external event is received. + /// /// public abstract Task WaitForExternalEventAsync(string eventName, TimeSpan timeout); From e7a71c423a4883b05eab01b15137f772c52e579a Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Thu, 25 May 2023 02:35:33 +0530 Subject: [PATCH 07/84] removing Obsolete attribute from config API classes (#1098) Signed-off-by: Shivam Kumar Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .../ConfigurationApi/Controllers/ConfigurationController.cs | 1 - examples/Client/ConfigurationApi/Program.cs | 3 +-- src/Dapr.Client/DaprClient.cs | 3 --- src/Dapr.Client/DaprClientGrpc.cs | 3 --- src/Dapr.Client/SubscribeConfigurationResponse.cs | 1 - .../DaprConfigurationStoreExtension.cs | 1 - .../DaprConfigurationStoreProvider.cs | 1 - .../DaprConfigurationStoreSource.cs | 1 - test/Dapr.Client.Test/ConfigurationApiTest.cs | 1 - test/Dapr.Client.Test/ConfigurationSourceTest.cs | 1 - 10 files changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs index 387c3f94f..55bf6df53 100644 --- a/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs +++ b/examples/Client/ConfigurationApi/Controllers/ConfigurationController.cs @@ -13,7 +13,6 @@ namespace ConfigurationApi.Controllers { [ApiController] [Route("configuration")] - [Obsolete] public class ConfigurationController : ControllerBase { private ILogger logger; diff --git a/examples/Client/ConfigurationApi/Program.cs b/examples/Client/ConfigurationApi/Program.cs index 12328ebdb..f5218602d 100644 --- a/examples/Client/ConfigurationApi/Program.cs +++ b/examples/Client/ConfigurationApi/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Dapr.Client; @@ -10,7 +10,6 @@ namespace ConfigurationApi { public class Program { - [Obsolete] public static void Main(string[] args) { Console.WriteLine("Starting application."); diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index de0813eb8..2a7299c78 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -907,7 +907,6 @@ public abstract Task>> GetBulkSecr /// Optional metadata that will be sent to the configuration store being queried. /// A that can be used to cancel the operation. /// A containing a - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task GetConfiguration( string storeName, IReadOnlyList keys, @@ -922,7 +921,6 @@ public abstract Task GetConfiguration( /// Optional metadata that will be sent to the configuration store being queried. /// A that can be used to cancel the operation. /// A which contains a reference to the stream. - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task SubscribeConfiguration( string storeName, IReadOnlyList keys, @@ -936,7 +934,6 @@ public abstract Task SubscribeConfiguration( /// The Id of the subscription that should no longer be watched. /// A that can be used to cancel the operation. /// - [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] public abstract Task UnsubscribeConfiguration( string storeName, string id, diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index a3bfdd340..c1cad41e1 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -1286,7 +1286,6 @@ public async override Task>> GetBu #region Configuration API /// - [Obsolete] public async override Task GetConfiguration( string storeName, IReadOnlyList keys, @@ -1330,7 +1329,6 @@ public async override Task GetConfiguration( } /// - [Obsolete] public override Task SubscribeConfiguration( string storeName, IReadOnlyList keys, @@ -1361,7 +1359,6 @@ public override Task SubscribeConfiguration( return Task.FromResult(new SubscribeConfigurationResponse(new DaprSubscribeConfigurationSource(client.SubscribeConfiguration(request, options)))); } - [Obsolete] public override async Task UnsubscribeConfiguration( string storeName, string id, diff --git a/src/Dapr.Client/SubscribeConfigurationResponse.cs b/src/Dapr.Client/SubscribeConfigurationResponse.cs index 1a3179dee..b371bd21c 100644 --- a/src/Dapr.Client/SubscribeConfigurationResponse.cs +++ b/src/Dapr.Client/SubscribeConfigurationResponse.cs @@ -6,7 +6,6 @@ namespace Dapr.Client /// /// Response for a Subscribe Configuration request. /// - [Obsolete("This response utilizes an alpha API which is subject to change. This attribute will be removed when the API is no longer Alpha.")] public class SubscribeConfigurationResponse { private ConfigurationSource source; diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs index f2b132bfe..190acacd1 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreExtension.cs @@ -22,7 +22,6 @@ namespace Dapr.Extensions.Configuration /// /// Extension used to call the Dapr Configuration API and store the values in a . /// - [Obsolete] public static class DaprConfigurationStoreExtension { /// diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs index a48e6175a..ea00f6c43 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreProvider.cs @@ -24,7 +24,6 @@ namespace Dapr.Extensions.Configuration /// A configuration provider that utilizes the Dapr Configuration API. It can either be a single, constant /// call or a streaming call. /// - [Obsolete] internal class DaprConfigurationStoreProvider : ConfigurationProvider, IDisposable { private string store; diff --git a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs index 2107c9101..2007ebc5b 100644 --- a/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs +++ b/src/Dapr.Extensions.Configuration/DaprConfigurationStoreSource.cs @@ -22,7 +22,6 @@ namespace Dapr.Extensions.Configuration /// /// Configuration source that provides a . /// - [Obsolete] public class DaprConfigurationStoreSource : IConfigurationSource { /// diff --git a/test/Dapr.Client.Test/ConfigurationApiTest.cs b/test/Dapr.Client.Test/ConfigurationApiTest.cs index cf81da95a..2e96582bd 100644 --- a/test/Dapr.Client.Test/ConfigurationApiTest.cs +++ b/test/Dapr.Client.Test/ConfigurationApiTest.cs @@ -20,7 +20,6 @@ namespace Dapr.Client.Test { - [System.Obsolete] public class ConfigurationApiTest { [Fact] diff --git a/test/Dapr.Client.Test/ConfigurationSourceTest.cs b/test/Dapr.Client.Test/ConfigurationSourceTest.cs index af5d05a45..c381d41de 100644 --- a/test/Dapr.Client.Test/ConfigurationSourceTest.cs +++ b/test/Dapr.Client.Test/ConfigurationSourceTest.cs @@ -9,7 +9,6 @@ namespace Dapr.Client.Test { - [Obsolete] public class ConfigurationSourceTest { private readonly string StoreName = "testStore"; From a4f5fc022d9df9b6db77213b7045b843235c882c Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Wed, 24 May 2023 17:55:30 -0400 Subject: [PATCH 08/84] add demo to how to (#1099) Signed-off-by: Hannah Hunter Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .../dotnet-workflow/dotnet-workflow-howto.md | 6 ++++++ 1 file changed, 6 insertions(+) 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 c1c792bc5..650cdec38 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 @@ -162,6 +162,12 @@ info: WorkflowConsoleApp.Activities.NotifyActivity[0] If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). +## Demo + +Watch this video [demonstrating .NET Workflow](https://youtu.be/BxiKpEmchgQ?t=2557): + + + ## Next steps - [Try the Dapr Workflow quickstart]({{< ref workflow-quickstart.md >}}) From edb09a08b7a2ca63983f5237b307c40cae86d3bb Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Fri, 26 May 2023 01:23:45 +0530 Subject: [PATCH 09/84] removing alpha for config api in docs (#1100) Signed-off-by: Shivam Kumar --- daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index beeb76f70..f68028dc5 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -165,7 +165,7 @@ Console.WriteLine("Got a secret value, I'm not going to be print it, it's a secr - For a full guide on secrets visit [How-To: Retrieve secrets]({{< ref howto-secrets.md >}}). -### Get Configuration Keys (Alpha) +### Get Configuration Keys ```csharp var client = new DaprClientBuilder().Build(); @@ -182,7 +182,7 @@ foreach (var item in configItems) } ``` -### Subscribe to Configuration Keys (Alpha) +### Subscribe to Configuration Keys ```csharp var client = new DaprClientBuilder().Build(); From ece9fbe0d4378523a3490a61e1cb4a591672d327 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Fri, 9 Jun 2023 21:50:51 +0530 Subject: [PATCH 10/84] adding get actor reminder API (#1103) * get actor reminder API Signed-off-by: Shivam Kumar * handling serialization better Signed-off-by: Shivam Kumar --------- Signed-off-by: Shivam Kumar --- examples/Actor/ActorClient/Program.cs | 13 +++++ examples/Actor/DemoActor/DemoActor.cs | 5 ++ examples/Actor/IDemoActor/IDemoActor.cs | 8 +++ src/Dapr.Actors/DaprHttpInteractor.cs | 17 ++++++ src/Dapr.Actors/IDaprInteractor.cs | 10 ++++ src/Dapr.Actors/Runtime/Actor.cs | 12 +++++ src/Dapr.Actors/Runtime/ActorTestOptions.cs | 5 ++ src/Dapr.Actors/Runtime/ActorTimerManager.cs | 7 +++ .../Runtime/DefaultActorTimerManager.cs | 30 +++++++++++ src/Dapr.Actors/Runtime/ReminderInfo.cs | 7 ++- test/Dapr.Actors.Test/ActorUnitTestTests.cs | 13 +++++ test/Dapr.Actors.Test/TestDaprInteractor.cs | 14 +++++ .../Reminders/IReminderActor.cs | 2 + .../Dapr.E2E.Test.App/Actors/ReminderActor.cs | 6 +++ .../Actors/E2ETests.ReminderTests.cs | 53 +++++++++++++++++++ 15 files changed, 200 insertions(+), 2 deletions(-) diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index 103aed6b9..aeee28386 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -96,6 +96,10 @@ public static async Task Main(string[] args) receivedData = await proxy.GetData(); Console.WriteLine($"Received data is {receivedData}."); + Console.WriteLine("Getting details of the registered reminder"); + var reminder = await proxy.GetReminder(); + Console.WriteLine($"Received reminder is {reminder}."); + Console.WriteLine("Deregistering timer. Timers would any way stop if the actor is deactivated as part of Dapr garbage collection."); await proxy.UnregisterTimer(); Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); @@ -105,14 +109,23 @@ public static async Task Main(string[] args) await proxy.RegisterReminderWithRepetitions(3); Console.WriteLine("Waiting so the reminder can be triggered"); await Task.Delay(5000); + Console.WriteLine("Getting details of the registered reminder"); + reminder = await proxy.GetReminder(); + Console.WriteLine($"Received reminder is {reminder}."); Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times."); await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2); + Console.WriteLine("Getting details of the registered reminder"); + reminder = await proxy.GetReminder(); + Console.WriteLine($"Received reminder is {reminder}."); Console.WriteLine("Deregistering reminder. Reminders are durable and would not stop until an explicit deregistration or the actor is deleted."); await proxy.UnregisterReminder(); Console.WriteLine("Registering reminder and Timer with TTL - The reminder will self delete after 10 seconds."); await proxy.RegisterReminderWithTtl(TimeSpan.FromSeconds(10)); await proxy.RegisterTimerWithTtl(TimeSpan.FromSeconds(10)); + Console.WriteLine("Getting details of the registered reminder"); + reminder = await proxy.GetReminder(); + Console.WriteLine($"Received reminder is {reminder}."); // Track the reminder. var timer = new Timer(async state => Console.WriteLine($"Received data: {await proxy.GetData()}"), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index 057b7df6d..0ab633fcd 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -85,6 +85,11 @@ public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repeti await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl); } + public async Task GetReminder() + { + return await this.GetReminderAsync("TestReminder"); + } + public Task UnregisterReminder() { return this.UnregisterReminderAsync("TestReminder"); diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs index 3220dfdbd..adec6df68 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -16,6 +16,7 @@ namespace IDemoActorInterface using System; using System.Threading.Tasks; using Dapr.Actors; + using Dapr.Actors.Runtime; /// /// Interface for Actor method. @@ -94,6 +95,13 @@ public interface IDemoActor : IActor /// A task that represents the asynchronous save operation. Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); + /// + /// Gets the registered reminder. + /// + /// The name of the reminder. + /// A task that returns the reminder after completion. + Task GetReminder(); + /// /// Unregisters the registered timer. /// diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index df5207f4e..410925dae 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -254,6 +254,23 @@ HttpRequestMessage RequestFunc() return this.SendAsync(RequestFunc, relativeUrl, cancellationToken); } + public async Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) + { + var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); + + HttpRequestMessage RequestFunc() + { + var request = new HttpRequestMessage() + { + Method = HttpMethod.Get, + }; + return request; + } + + var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + return await response.Content.ReadAsStreamAsync(); + } + public Task UnregisterReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default) { var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorReminderRelativeUrlFormat, actorType, actorId, reminderName); diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 04eb66de9..8f30aa18f 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -74,6 +74,16 @@ internal interface IDaprInteractor /// A representing the result of the asynchronous operation. Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default); + /// + /// Gets a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + Task GetReminderAsync(string actorType, string actorId, string reminderName, CancellationToken cancellationToken = default); + /// /// Unregisters a reminder. /// diff --git a/src/Dapr.Actors/Runtime/Actor.cs b/src/Dapr.Actors/Runtime/Actor.cs index 0f74513a1..d7291cb58 100644 --- a/src/Dapr.Actors/Runtime/Actor.cs +++ b/src/Dapr.Actors/Runtime/Actor.cs @@ -360,6 +360,18 @@ internal async Task RegisterReminderAsync(ActorReminderOptions o return reminder; } + /// + /// Gets a reminder previously registered using . + /// + /// The name of the reminder to get. + /// + /// Returns a task that represents the asynchronous get operation. The result of the task contains the reminder if it exists, otherwise null. + /// + protected async Task GetReminderAsync(string reminderName) + { + return await this.Host.TimerManager.GetReminderAsync(new ActorReminderToken(this.actorTypeName, this.Id, reminderName)); + } + /// /// Unregisters a reminder previously registered using . /// diff --git a/src/Dapr.Actors/Runtime/ActorTestOptions.cs b/src/Dapr.Actors/Runtime/ActorTestOptions.cs index 1a1fa6f2e..47e73ce36 100644 --- a/src/Dapr.Actors/Runtime/ActorTestOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorTestOptions.cs @@ -91,6 +91,11 @@ public override Task RegisterTimerAsync(ActorTimer timer) throw new NotImplementedException(Message); } + public override Task GetReminderAsync(ActorReminderToken reminder) + { + throw new NotImplementedException(Message); + } + public override Task UnregisterReminderAsync(ActorReminderToken reminder) { throw new NotImplementedException(Message); diff --git a/src/Dapr.Actors/Runtime/ActorTimerManager.cs b/src/Dapr.Actors/Runtime/ActorTimerManager.cs index 1dc304fd1..784cf418e 100644 --- a/src/Dapr.Actors/Runtime/ActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/ActorTimerManager.cs @@ -27,6 +27,13 @@ public abstract class ActorTimerManager /// A task which will complete when the operation completes. public abstract Task RegisterReminderAsync(ActorReminder reminder); + /// + /// Gets a reminder previously registered using + /// + /// The to unregister. + /// A task which will complete when the operation completes. + public abstract Task GetReminderAsync(ActorReminderToken reminder); + /// /// Unregisters the provided reminder with the runtime. /// diff --git a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs index d3378c962..b42b432a1 100644 --- a/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs +++ b/src/Dapr.Actors/Runtime/DefaultActorTimerManager.cs @@ -14,6 +14,8 @@ using System; using System.Text.Json; using System.Threading.Tasks; +using System.IO; +using System.Text; namespace Dapr.Actors.Runtime { @@ -37,6 +39,18 @@ public override async Task RegisterReminderAsync(ActorReminder reminder) await this.interactor.RegisterReminderAsync(reminder.ActorType, reminder.ActorId.ToString(), reminder.Name, serialized); } + public override async Task GetReminderAsync(ActorReminderToken token) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + var responseStream = await this.interactor.GetReminderAsync(token.ActorType, token.ActorId.ToString(), token.Name); + var reminder = await DeserializeReminderAsync(responseStream, token); + return reminder; + } + public override async Task UnregisterReminderAsync(ActorReminderToken reminder) { if (reminder == null) @@ -77,5 +91,21 @@ private async ValueTask SerializeReminderAsync(ActorReminder reminder) reminder.Ttl); return await info.SerializeAsync(); } + + private async ValueTask DeserializeReminderAsync(Stream stream, ActorReminderToken token) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + var info = await ReminderInfo.DeserializeAsync(stream); + if(info == null) + { + return null; + } + var reminder = new ActorReminder(token.ActorType, token.ActorId, token.Name, info.Data, info.DueTime, + info.Period); + return reminder; + } } } diff --git a/src/Dapr.Actors/Runtime/ReminderInfo.cs b/src/Dapr.Actors/Runtime/ReminderInfo.cs index 84e56bbc7..447cf607b 100644 --- a/src/Dapr.Actors/Runtime/ReminderInfo.cs +++ b/src/Dapr.Actors/Runtime/ReminderInfo.cs @@ -20,7 +20,7 @@ namespace Dapr.Actors.Runtime using System.Threading.Tasks; // represents the wire format used by Dapr to store reminder info with the runtime - internal struct ReminderInfo + internal class ReminderInfo { public ReminderInfo( byte[] data, @@ -49,13 +49,16 @@ public ReminderInfo( internal static async Task DeserializeAsync(Stream stream) { var json = await JsonSerializer.DeserializeAsync(stream); + if(json.ValueKind == JsonValueKind.Null) + { + return null; + } var dueTime = default(TimeSpan); var period = default(TimeSpan); var data = default(byte[]); int? repetition = null; TimeSpan? ttl = null; - if (json.TryGetProperty("dueTime", out var dueTimeProperty)) { var dueTimeString = dueTimeProperty.GetString(); diff --git a/test/Dapr.Actors.Test/ActorUnitTestTests.cs b/test/Dapr.Actors.Test/ActorUnitTestTests.cs index 318ede9fc..baa52c568 100644 --- a/test/Dapr.Actors.Test/ActorUnitTestTests.cs +++ b/test/Dapr.Actors.Test/ActorUnitTestTests.cs @@ -74,6 +74,7 @@ public async Task CanTestStartingAndStoppingTimer() public async Task CanTestStartingAndStoppinReminder() { var reminders = new List(); + IActorReminder getReminder = null; var timerManager = new Mock(MockBehavior.Strict); timerManager @@ -84,6 +85,9 @@ public async Task CanTestStartingAndStoppinReminder() .Setup(tm => tm.UnregisterReminderAsync(It.IsAny())) .Callback(reminder => reminders.RemoveAll(t => t.Name == reminder.Name)) .Returns(Task.CompletedTask); + timerManager + .Setup(tm => tm.GetReminderAsync(It.IsAny())) + .Returns(() => Task.FromResult(getReminder)); var host = ActorHost.CreateForTest(new ActorTestOptions(){ TimerManager = timerManager.Object, }); var actor = new CoolTestActor(host); @@ -109,6 +113,10 @@ public async Task CanTestStartingAndStoppinReminder() await actor.ReceiveReminderAsync(reminder.Name, reminder.State, reminder.DueTime, reminder.Period); } + getReminder = reminder; + var reminderFromGet = await actor.GetReminderAsync(); + Assert.Equal(reminder, reminderFromGet); + // Stop the reminder await actor.StopReminderAsync(); Assert.Empty(reminders); @@ -148,6 +156,11 @@ public async Task StartReminderAsync(Message message) await this.RegisterReminderAsync("record", bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(5)); } + public async Task GetReminderAsync() + { + return await this.GetReminderAsync("record"); + } + public async Task StopReminderAsync() { await this.UnregisterReminderAsync("record"); diff --git a/test/Dapr.Actors.Test/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 1b382208d..92cfa7096 100644 --- a/test/Dapr.Actors.Test/TestDaprInteractor.cs +++ b/test/Dapr.Actors.Test/TestDaprInteractor.cs @@ -99,6 +99,20 @@ Task IDaprInteractor.InvokeActorMethodWithRemotingAsync(A throw new System.NotImplementedException(); } + /// + /// Gets a reminder. + /// + /// Type of actor. + /// ActorId. + /// Name of reminder to unregister. + /// Cancels the operation. + /// A representing the result of the asynchronous operation. + public Task GetReminderAsync(string actorType, string actorId, string reminderName, + CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); + } + /// /// Unregisters a reminder. /// diff --git a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs index 33a5e0d05..0bf57f64c 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs @@ -28,5 +28,7 @@ public interface IReminderActor : IPingActor, IActor Task StartReminderWithTtlAndRepetitions(TimeSpan ttl, int repetitions); Task GetState(); + + Task GetReminder(); } } diff --git a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs index f9b9d7573..b08e483c2 100644 --- a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs @@ -44,6 +44,12 @@ public async Task StartReminder(StartReminderOptions options) await this.StateManager.SetStateAsync("reminder-state", new State(){ IsReminderRunning = true, }); } + public async Task GetReminder(){ + var reminder = await this.GetReminderAsync("test-reminder"); + var reminderString = JsonSerializer.Serialize(reminder, this.Host.JsonSerializerOptions); + return reminderString; + } + public async Task StartReminderWithTtl(TimeSpan ttl) { var options = new StartReminderOptions() diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs index 50cd87219..ff39cce8a 100644 --- a/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs +++ b/test/Dapr.E2E.Test/Actors/E2ETests.ReminderTests.cs @@ -13,6 +13,7 @@ namespace Dapr.E2E.Test { using System; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dapr.Actors; @@ -49,6 +50,58 @@ public async Task ActorCanStartAndStopReminder() Assert.Equal(10, state.Count); } + [Fact] + public async Task ActorCanStartAndStopAndGetReminder() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "ReminderActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Get reminder before starting it, should return null. + var reminder = await proxy.GetReminder(); + Assert.Equal("null", reminder); + + // Start reminder, to count up to 10 + await proxy.StartReminder(new StartReminderOptions(){ Total = 10, }); + + State state = new State(); + var countGetReminder = 0; + while (true) + { + cts.Token.ThrowIfCancellationRequested(); + + reminder = await proxy.GetReminder(); + Assert.NotNull(reminder); + + // If reminder is null then it means the reminder has been stopped. + if (reminder != "null") + { + countGetReminder++; + var reminderJson = JsonSerializer.Deserialize(reminder); + var name = reminderJson.GetProperty("name").ToString(); + var period = reminderJson.GetProperty("period").ToString(); + var dueTime = reminderJson.GetProperty("dueTime").ToString(); + + Assert.Equal("test-reminder", name); + Assert.Equal(TimeSpan.FromMilliseconds(50).ToString(), period); + Assert.Equal(TimeSpan.Zero.ToString(), dueTime); + } + + state = await proxy.GetState(); + this.Output.WriteLine($"Got Count: {state.Count} IsReminderRunning: {state.IsReminderRunning}"); + if (!state.IsReminderRunning) + { + break; + } + } + + // Should count up to exactly 10 + Assert.Equal(10, state.Count); + // Should be able to Get Reminder at least once. + Assert.True(countGetReminder > 0); + } + [Fact] public async Task ActorCanStartReminderWithRepetitions() { From be959f943c7c374288ba12d33c4f419c65c57b55 Mon Sep 17 00:00:00 2001 From: Bernd Verst Date: Thu, 15 Jun 2023 16:16:32 -0700 Subject: [PATCH 11/84] Link to DotNet Fossa status (#1105) Link to DotNet Fossa status Signed-off-by: Bernd Verst Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54bb606dc..d7232c54e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://github.com/dapr/dotnet-sdk/workflows/build/badge.svg)](https://github.com/dapr/dotnet-sdk/actions?workflow=build) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) [![License: Apache](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdapr%2Fcomponents-contrib.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdapr%2Fcomponents-contrib?ref=badge_shield) +[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git?ref=badge_shield) Dapr SDK for .NET allows you to: From 8e9db70c0f58050f44970cda003297f561ab570a Mon Sep 17 00:00:00 2001 From: Shubham Sharma Date: Fri, 16 Jun 2023 05:37:36 +0530 Subject: [PATCH 12/84] Fix HTTP examples in Workflow Console App (#1107) * Update demo.http; Signed-off-by: Shubham Sharma * Fix input Signed-off-by: Shubham Sharma --------- Signed-off-by: Shubham Sharma Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- examples/Workflow/WorkflowConsoleApp/demo.http | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/Workflow/WorkflowConsoleApp/demo.http b/examples/Workflow/WorkflowConsoleApp/demo.http index bb91e5045..48b849a88 100644 --- a/examples/Workflow/WorkflowConsoleApp/demo.http +++ b/examples/Workflow/WorkflowConsoleApp/demo.http @@ -1,17 +1,17 @@ ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start +POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json -{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}} +{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1} ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start +POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json -{ "input" : {"Name": "Cars", "TotalCost": 10000, "Quantity": 30}} +{"Name": "Cars", "TotalCost": 10000, "Quantity": 30} ### Query dapr sidecar - replace xxx with id from the workflow you've created above -GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx +GET http://localhost:3500/v1.0-alpha1/workflows/dapr/xxx ### Terminate the workflow - replace xxx with id from the workflow you've created above POST http://localhost:3500/v1.0-alpha1/workflows/dapr/xxx/terminate \ No newline at end of file From c99475be3c415dd050d49165c12efb7a872e791f Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 7 Jul 2023 08:44:40 +0900 Subject: [PATCH 13/84] [Workflow] Fix issue with ignored external event payload (#1119) * [Workflow] Fix issue with ignored external event payload Signed-off-by: Chris Gillum * Pushing missing commits Signed-off-by: Chris Gillum * Remove unnecessary steps from itests.yml Signed-off-by: Chris Gillum --------- Signed-off-by: Chris Gillum --- .github/workflows/itests.yml | 10 ---------- src/Dapr.Workflow/Dapr.Workflow.csproj | 6 +++--- src/Dapr.Workflow/DaprWorkflowClient.cs | 12 ++++++------ 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 0044fec93..3f42b6bd2 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -105,16 +105,6 @@ jobs: - uses: actions/checkout@v1 - name: Parse release version run: python ./.github/scripts/get_release_version.py - - name: Install Local kafka using docker-compose - run: | - docker-compose -f test/Dapr.E2E.Test/deploy/local-test-kafka.yml up -d - docker ps - - name: Install Local Hashicorp Vault using docker-compose - run: | - docker-compose -f test/Dapr.E2E.Test/deploy/local-test-vault.yml up -d - docker ps - - name: Setup Vault's test token - run: echo myroot > /tmp/.hashicorp_vault_token - name: Setup ${{ matrix.display-name }} uses: actions/setup-dotnet@v1 with: diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index a951350e8..f6c28b02f 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -7,14 +7,14 @@ Dapr.Workflow Dapr Workflow Authoring SDK Dapr Workflow SDK for building workflows as code with Dapr - 0.2.0 + 0.3.0 alpha 10.0 - - + + diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index 249de09b7..4c4902dbb 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -70,9 +70,9 @@ public Task ScheduleNewWorkflowAsync( /// The unique ID of the workflow instance to fetch. /// /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to - /// omit them. Defaults to false. + /// omit them. Defaults to true. /// - public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = false) + public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = true) { OrchestrationMetadata? metadata = await this.innerClient.GetInstancesAsync( instanceId, @@ -94,7 +94,7 @@ public async Task GetWorkflowStateAsync(string instanceId, bool g /// The unique ID of the workflow instance to wait for. /// /// Specify true to fetch the workflow instance's inputs, outputs, and custom status, or false to - /// omit them. The default value is false to minimize the network bandwidth, serialization, and memory costs + /// omit them. Setting this value to false can help minimize the network bandwidth, serialization, and memory costs /// associated with fetching the instance metadata. /// /// A that can be used to cancel the wait operation. @@ -104,7 +104,7 @@ public async Task GetWorkflowStateAsync(string instanceId, bool g /// public async Task WaitForWorkflowStartAsync( string instanceId, - bool getInputsAndOutputs = false, + bool getInputsAndOutputs = true, CancellationToken cancellation = default) { OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceStartAsync( @@ -135,7 +135,7 @@ public async Task WaitForWorkflowStartAsync( /// public async Task WaitForWorkflowCompletionAsync( string instanceId, - bool getInputsAndOutputs = false, + bool getInputsAndOutputs = true, CancellationToken cancellation = default) { OrchestrationMetadata metadata = await this.innerClient.WaitForInstanceCompletionAsync( @@ -218,7 +218,7 @@ public Task RaiseEventAsync( object? eventPayload = null, CancellationToken cancellation = default) { - return this.innerClient.RaiseEventAsync(instanceId, eventName, cancellation); + return this.innerClient.RaiseEventAsync(instanceId, eventName, eventPayload, cancellation); } /// From 574dc0cb3dae726bb3cccdb231627ef71e8147ab Mon Sep 17 00:00:00 2001 From: MonkeyTennis Date: Tue, 18 Jul 2023 19:10:11 +0100 Subject: [PATCH 14/84] Rev'ed Grpc.Net.Client PackageReference version for Dapr dotnet-sdk (#1126) Rev'ed Grpc.Net.Client PackageReference version for Dapr dotnet-sdk Signed-off-by: Bradley Cotier --- examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj | 2 +- src/Dapr.Client/Dapr.Client.csproj | 2 +- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 23ead7d4d..5ce78bc57 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 7c9ff8160..59a38a3e4 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -21,7 +21,7 @@ - + diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index c94f031e9..36f9e725b 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -11,7 +11,7 @@ - + From f788efabdee06f4ff139e0a390d347b454608e6e Mon Sep 17 00:00:00 2001 From: Artur Souza Date: Tue, 18 Jul 2023 11:25:17 -0700 Subject: [PATCH 15/84] Add support to DAPR_HTTP_ENDPOINT and DAPR_GRPC_ENDPOINT env. (#1124) Signed-off-by: Artur Souza --- .../dotnet-client/dotnet-daprclient-usage.md | 6 ++++-- .../Runtime/ActorRuntimeOptions.cs | 5 +++-- src/Dapr.Client/DaprClientBuilder.cs | 5 +++-- .../WorkflowServiceCollectionExtensions.cs | 6 ++++++ src/Shared/DaprDefaults.cs | 20 +++++++++++++++---- 5 files changed, 32 insertions(+), 10 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md index 00b330693..26328050c 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/dotnet-daprclient-usage.md @@ -34,8 +34,10 @@ The `DaprClientBuilder` contains settings for: The SDK will read the following environment variables to configure the default values: -- `DAPR_HTTP_PORT`: used to find the HTTP endpoint of the Dapr sidecar -- `DAPR_GRPC_PORT`: used to find the gRPC endpoint of the Dapr sidecar +- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com` +- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com` +- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar +- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar - `DAPR_API_TOKEN`: used to set the API Token ### Configuring gRPC channel options diff --git a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs index 3f4a6df88..30ee7b476 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs @@ -220,8 +220,9 @@ public int? RemindersStoragePartitions /// /// /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the - /// DAPR_HTTP_PORT environment variable. + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. /// /// public string HttpEndpoint { get; set; } = DaprDefaults.GetDefaultHttpEndpoint(); diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 5b484e208..1580afb36 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -63,8 +63,9 @@ public DaprClientBuilder() /// /// /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_HTTP_PORT where DAPR_HTTP_PORT represents the value of the - /// DAPR_HTTP_PORT environment variable. + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. /// /// The instance. public DaprClientBuilder UseHttpEndpoint(string httpEndpoint) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 161c29175..c24265475 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -104,6 +104,12 @@ static bool TryGetGrpcAddress(out string address) // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. + string? daprEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + if (!String.IsNullOrEmpty(daprEndpoint)) { + address = daprEndpoint; + return true; + } + string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); if (int.TryParse(daprPortStr, out int daprGrpcPort)) { diff --git a/src/Shared/DaprDefaults.cs b/src/Shared/DaprDefaults.cs index 28c02c148..1ddab49b0 100644 --- a/src/Shared/DaprDefaults.cs +++ b/src/Shared/DaprDefaults.cs @@ -57,13 +57,19 @@ public static string GetDefaultAppApiToken() } /// - /// Get the value of environment variable DAPR_HTTP_PORT + /// Get the value of HTTP endpoint based off environment variables /// - /// The value of environment variable DAPR_HTTP_PORT + /// The value of HTTP endpoint based off environment variables public static string GetDefaultHttpEndpoint() { if (httpEndpoint == null) { + var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) { + httpEndpoint = endpoint; + return httpEndpoint; + } + var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); port = string.IsNullOrEmpty(port) ? "3500" : port; httpEndpoint = $"http://127.0.0.1:{port}"; @@ -73,13 +79,19 @@ public static string GetDefaultHttpEndpoint() } /// - /// Get the value of environment variable DAPR_GRPC_PORT + /// Get the value of gRPC endpoint based off environment variables /// - /// The value of environment variable DAPR_GRPC_PORT + /// The value of gRPC endpoint based off environment variables public static string GetDefaultGrpcEndpoint() { if (grpcEndpoint == null) { + var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) { + grpcEndpoint = endpoint; + return grpcEndpoint; + } + var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); port = string.IsNullOrEmpty(port) ? "50001" : port; grpcEndpoint = $"http://127.0.0.1:{port}"; From 6dae4e339dff5965c936b1bb62b8ef793faad8c6 Mon Sep 17 00:00:00 2001 From: Aaron Crawfis Date: Tue, 18 Jul 2023 11:39:16 -0700 Subject: [PATCH 16/84] Add cascading metadata (#1128) Signed-off-by: Aaron Crawfis --- daprdocs/content/en/dotnet-sdk-docs/_index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 65d818723..e823ca29f 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -5,6 +5,11 @@ linkTitle: ".NET" weight: 1000 description: .NET SDK packages for developing Dapr applications no_list: true +cascade: + github_repo: https://github.com/dapr/dotnet-sdk + github_subdir: daprdocs/content/en/dotnet-sdk-docs + path_base_for_github_subdir: content/en/developing-applications/sdks/dotnet/ + github_branch: master --- Dapr offers a variety of packages to help with the development of .NET applications. Using them you can create .NET clients, servers, and virtual actors with Dapr. From 2449bcd6691eb49825e0e8e9dff50bd50fd41c2e Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:51:12 -0400 Subject: [PATCH 17/84] remove invalid code line (#1127) Signed-off-by: Hannah Hunter --- .../dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index 986562ca8..5b79d6b8f 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 @@ -306,14 +306,6 @@ namespace MyActorService { app.UseDeveloperExceptionPage(); } - else - { - // By default, ASP.Net Core uses port 5000 for HTTP. The HTTP - // redirection will interfere with the Dapr runtime. You can - // move this out of the else block if you use port 5001 in this - // example, and developer tooling (such as the VSCode extension). - app.UseHttpsRedirection(); - } app.UseRouting(); From f4e02df980072438ad225b75412f779df67936a0 Mon Sep 17 00:00:00 2001 From: Shivam Kumar Date: Sat, 22 Jul 2023 02:45:42 +0530 Subject: [PATCH 18/84] adding get actor reminder API in docs (#1113) Signed-off-by: Shivam Kumar --- .../dotnet-actors/dotnet-actors-howto.md | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 5b79d6b8f..ab41c3917 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 @@ -81,6 +81,7 @@ Define `IMyActor` interface and `MyData` data object. Paste the following code i ```csharp using Dapr.Actors; +using Dapr.Actors.Runtime; using System.Threading.Tasks; namespace MyActor.Interfaces @@ -91,6 +92,7 @@ namespace MyActor.Interfaces Task GetDataAsync(); Task RegisterReminder(); Task UnregisterReminder(); + Task GetReminder(); Task RegisterTimer(); Task UnregisterTimer(); } @@ -219,6 +221,14 @@ namespace MyActorService TimeSpan.FromSeconds(5)); // Time interval between reminder invocations after the first invocation } + /// + /// Get MyReminder reminder details with the actor + /// + public async Task GetReminder() + { + await this.GetReminderAsync("MyReminder"); + } + /// /// Unregister MyReminder reminder with the actor /// From 667dcaf44154bc41611bf3b19a048589eabe2ce8 Mon Sep 17 00:00:00 2001 From: vlardn Date: Wed, 16 Aug 2023 19:49:12 +0300 Subject: [PATCH 19/84] Inroduce OnActorMethodFailedAsync virtual method for overriding (#1014) Signed-off-by: Vlad Rudenko --- src/Dapr.Actors/Runtime/Actor.cs | 31 ++++++++++++++++++++++--- src/Dapr.Actors/Runtime/ActorManager.cs | 6 ++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Actors/Runtime/Actor.cs b/src/Dapr.Actors/Runtime/Actor.cs index d7291cb58..ddfc266e9 100644 --- a/src/Dapr.Actors/Runtime/Actor.cs +++ b/src/Dapr.Actors/Runtime/Actor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -101,10 +101,11 @@ internal async Task OnPostActorMethodAsyncInternal(ActorMethodContext actorMetho await this.SaveStateAsync(); } - internal Task OnInvokeFailedAsync() + internal async Task OnActorMethodFailedInternalAsync(ActorMethodContext actorMethodContext, Exception e) { + await this.OnActorMethodFailedAsync(actorMethodContext, e); // Exception has been thrown by user code, reset the state in state manager. - return this.ResetStateAsync(); + await this.ResetStateAsync(); } internal Task ResetStateAsync() @@ -190,6 +191,30 @@ protected virtual Task OnPostActorMethodAsync(ActorMethodContext actorMethodCont return Task.CompletedTask; } + /// + /// Override this method for performing any action when invoking actor method has thrown an exception. + /// This method is invoked by actor runtime when invoking actor method has thrown an exception. + /// + /// + /// An describing the method that was invoked by actor runtime prior to this method. + /// + /// Exception thrown by either actor method or by OnPreActorMethodAsync/OnPostActorMethodAsync overriden methods. + /// + /// Returns a Task representing post-actor-method operation. + /// + /// /// + /// This method is invoked by actor runtime prior to: + /// + /// Invoking an actor interface method when a client request comes. + /// Invoking a method when a reminder fires. + /// Invoking a timer callback when timer fires. + /// + /// + protected virtual Task OnActorMethodFailedAsync(ActorMethodContext actorMethodContext, Exception e) + { + return Task.CompletedTask; + } + /// /// Registers a reminder with the actor. /// diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index e7f29f5cc..6278b0ded 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -355,9 +355,9 @@ private async Task DispatchInternalAsync(ActorId actorId, ActorMethodConte // PostActivate will save the state, its not invoked when actorFunc invocation throws. await actor.OnPostActorMethodAsyncInternal(actorMethodContext); } - catch (Exception) + catch (Exception e) { - await actor.OnInvokeFailedAsync(); + await actor.OnActorMethodFailedInternalAsync(actorMethodContext, e); throw; } finally From 17f849b17505b9a61be1e7bd3e69586718b9fdd3 Mon Sep 17 00:00:00 2001 From: Yash Nisar Date: Thu, 24 Aug 2023 13:04:02 -0500 Subject: [PATCH 20/84] Remove .NET Core 3.1 support and standardize on .NET 6 (#1045) * Remove .NET Core 3.1 support and standardize on .NET 6 Signed-off-by: Yash Nisar * Remove support for .NET 5 as well Signed-off-by: Yash Nisar --------- Signed-off-by: Yash Nisar --- .github/workflows/itests.yml | 10 +----- .github/workflows/sdk_build.yml | 8 +---- examples/Actor/ActorClient/ActorClient.csproj | 2 +- examples/Actor/DemoActor/DemoActor.csproj | 2 +- examples/Actor/IDemoActor/IDemoActor.csproj | 2 +- .../ControllerSample/ControllerSample.csproj | 2 +- .../AspNetCore/ControllerSample/README.md | 2 +- .../GrpcServiceSample.csproj | 2 +- .../AspNetCore/GrpcServiceSample/README.md | 2 +- examples/AspNetCore/RoutingSample/README.md | 2 +- .../README.md | 2 +- ...retStoreConfigurationProviderSample.csproj | 2 +- .../ConfigurationApi/ConfigurationApi.csproj | 2 +- examples/Client/ConfigurationApi/README.md | 2 +- .../DistributedLock/DistributedLock.csproj | 2 +- examples/Client/DistributedLock/README.md | 2 +- .../BulkPublishEventExample.csproj | 2 +- .../PublishEventExample.csproj | 2 +- examples/Client/ServiceInvocation/README.md | 2 +- .../ServiceInvocation.csproj | 2 +- examples/Client/StateManagement/README.md | 2 +- .../StateManagement/StateManagement.csproj | 2 +- properties/IsExternalInit.cs | 3 +- .../Dapr.Actors.AspNetCore.csproj | 2 +- .../Communication/ActorInvokeException.cs | 2 +- src/Dapr.Actors/Dapr.Actors.csproj | 2 +- src/Dapr.AspNetCore/Dapr.AspNetCore.csproj | 2 +- src/Dapr.Client/Dapr.Client.csproj | 2 +- src/Dapr.Client/DaprClientGrpc.cs | 32 ++++++++----------- src/Dapr.Client/InvocationHandler.cs | 2 +- .../Dapr.Extensions.Configuration.csproj | 2 +- src/Dapr.Workflow/Dapr.Workflow.csproj | 2 +- ...tors.AspNetCore.IntegrationTest.App.csproj | 2 +- ...r.Actors.AspNetCore.IntegrationTest.csproj | 2 +- .../Dapr.Actors.AspNetCore.Test.csproj | 2 +- test/Dapr.Actors.Test/Dapr.Actors.Test.csproj | 2 +- ...Dapr.AspNetCore.IntegrationTest.App.csproj | 2 +- .../Dapr.AspNetCore.IntegrationTest.csproj | 2 +- .../Dapr.AspNetCore.Test.csproj | 2 +- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 2 +- .../Dapr.E2E.Test.Actors.csproj | 2 +- .../Dapr.E2E.Test.App.Grpc.csproj | 2 +- .../Dapr.E2E.Test.App.ReentrantActors.csproj | 2 +- .../Dapr.E2E.Test.App.csproj | 2 +- test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 2 +- test/Dapr.E2E.Test/DaprTestApp.cs | 10 +----- .../Dapr.Extensions.Configuration.Test.csproj | 2 +- 47 files changed, 60 insertions(+), 87 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 3f42b6bd2..870264f40 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -19,22 +19,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['3.1', '6.0', '7.0'] + dotnet-version: ['6.0', '7.0'] include: - - dotnet-version: '3.1' - install-3: true - display-name: '.NET Core 3.1' - framework: 'netcoreapp3.1' - prefix: 'netcoreapp31' - install-version: '3.1.x' # We always need a new .NET - dotnet-version: '6.0' - install-3: false display-name: '.NET 6.0' framework: 'net6' prefix: 'net6' install-version: '6.0.x' - dotnet-version: '7.0' - install-3: false display-name: '.NET 7.0' framework: 'net7' prefix: 'net7' diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index b0b6b042c..1c4e1a60e 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -42,14 +42,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['3.1', '6.0', '7.0'] + dotnet-version: ['6.0', '7.0'] include: - - dotnet-version: '3.1' - install-3: true - display-name: '.NET Core 3.1' - framework: 'netcoreapp3.1' - prefix: 'netcoreapp31' - install-version: '3.1.x' # We always need a new .NET - dotnet-version: '6.0' install-3: false display-name: '.NET 6.0' diff --git a/examples/Actor/ActorClient/ActorClient.csproj b/examples/Actor/ActorClient/ActorClient.csproj index 48c8318c6..0d1d94f55 100644 --- a/examples/Actor/ActorClient/ActorClient.csproj +++ b/examples/Actor/ActorClient/ActorClient.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 80a3883c3..1ee37fdbe 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/examples/Actor/IDemoActor/IDemoActor.csproj b/examples/Actor/IDemoActor/IDemoActor.csproj index 23a96ffc6..9f7744796 100644 --- a/examples/Actor/IDemoActor/IDemoActor.csproj +++ b/examples/Actor/IDemoActor/IDemoActor.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/examples/AspNetCore/ControllerSample/ControllerSample.csproj b/examples/AspNetCore/ControllerSample/ControllerSample.csproj index 863290324..6dbe750a6 100644 --- a/examples/AspNetCore/ControllerSample/ControllerSample.csproj +++ b/examples/AspNetCore/ControllerSample/ControllerSample.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/examples/AspNetCore/ControllerSample/README.md b/examples/AspNetCore/ControllerSample/README.md index a2700e94d..3b2ca02b9 100644 --- a/examples/AspNetCore/ControllerSample/README.md +++ b/examples/AspNetCore/ControllerSample/README.md @@ -12,7 +12,7 @@ The application also registers for pub/sub with the `deposit`, `multideposit` an ## Prerequisitess -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj index 5ce78bc57..123763489 100644 --- a/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj +++ b/examples/AspNetCore/GrpcServiceSample/GrpcServiceSample.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6 true diff --git a/examples/AspNetCore/GrpcServiceSample/README.md b/examples/AspNetCore/GrpcServiceSample/README.md index 3f27d280c..d08e96cd9 100644 --- a/examples/AspNetCore/GrpcServiceSample/README.md +++ b/examples/AspNetCore/GrpcServiceSample/README.md @@ -11,7 +11,7 @@ The application also registers for pub/sub with the `deposit` and `withdraw` top ## Prerequisitess -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/RoutingSample/README.md b/examples/AspNetCore/RoutingSample/README.md index cd545b747..901c51c40 100644 --- a/examples/AspNetCore/RoutingSample/README.md +++ b/examples/AspNetCore/RoutingSample/README.md @@ -12,7 +12,7 @@ The application also registers for pub/sub with the `deposit`, `multideposit`, a ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md index 5be9eb5e1..09422e474 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj index 10928470b..01fbc2079 100644 --- a/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj +++ b/examples/AspNetCore/SecretStoreConfigurationProviderSample/SecretStoreConfigurationProviderSample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 diff --git a/examples/Client/ConfigurationApi/ConfigurationApi.csproj b/examples/Client/ConfigurationApi/ConfigurationApi.csproj index f73fd4974..dee6a9878 100644 --- a/examples/Client/ConfigurationApi/ConfigurationApi.csproj +++ b/examples/Client/ConfigurationApi/ConfigurationApi.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6 diff --git a/examples/Client/ConfigurationApi/README.md b/examples/Client/ConfigurationApi/README.md index 41c1c831f..7425a780a 100644 --- a/examples/Client/ConfigurationApi/README.md +++ b/examples/Client/ConfigurationApi/README.md @@ -9,7 +9,7 @@ It demonstrates the following APIs: ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/DistributedLock/DistributedLock.csproj b/examples/Client/DistributedLock/DistributedLock.csproj index 180c46f28..9c3272e6e 100644 --- a/examples/Client/DistributedLock/DistributedLock.csproj +++ b/examples/Client/DistributedLock/DistributedLock.csproj @@ -6,7 +6,7 @@ - netcoreapp3.1 + net6 diff --git a/examples/Client/DistributedLock/README.md b/examples/Client/DistributedLock/README.md index 788d57e3a..cdac6f91a 100644 --- a/examples/Client/DistributedLock/README.md +++ b/examples/Client/DistributedLock/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj index 5c05faf9f..3f22acaf8 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/BulkPublishEventExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable diff --git a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj index f7d335879..2df4ec967 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj +++ b/examples/Client/PublishSubscribe/PublishEventExample/PublishEventExample.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable diff --git a/examples/Client/ServiceInvocation/README.md b/examples/Client/ServiceInvocation/README.md index 171869c15..ede5a506a 100644 --- a/examples/Client/ServiceInvocation/README.md +++ b/examples/Client/ServiceInvocation/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/ServiceInvocation/ServiceInvocation.csproj b/examples/Client/ServiceInvocation/ServiceInvocation.csproj index 790bfc53a..e3df962a1 100644 --- a/examples/Client/ServiceInvocation/ServiceInvocation.csproj +++ b/examples/Client/ServiceInvocation/ServiceInvocation.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable diff --git a/examples/Client/StateManagement/README.md b/examples/Client/StateManagement/README.md index 141f16760..fb266a242 100644 --- a/examples/Client/StateManagement/README.md +++ b/examples/Client/StateManagement/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/StateManagement/StateManagement.csproj b/examples/Client/StateManagement/StateManagement.csproj index 790bfc53a..e3df962a1 100644 --- a/examples/Client/StateManagement/StateManagement.csproj +++ b/examples/Client/StateManagement/StateManagement.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6 Samples.Client enable diff --git a/properties/IsExternalInit.cs b/properties/IsExternalInit.cs index 2f68a08f4..34357c39a 100644 --- a/properties/IsExternalInit.cs +++ b/properties/IsExternalInit.cs @@ -13,6 +13,5 @@ namespace System.Runtime.CompilerServices internal static class IsExternalInit { } - - // This is a polyfill for init only properties in netcoreapp3.1 + } diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index a9f6e8818..cfd7d8123 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net6 diff --git a/src/Dapr.Actors/Communication/ActorInvokeException.cs b/src/Dapr.Actors/Communication/ActorInvokeException.cs index ed17a7f55..ac4ea63df 100644 --- a/src/Dapr.Actors/Communication/ActorInvokeException.cs +++ b/src/Dapr.Actors/Communication/ActorInvokeException.cs @@ -93,7 +93,7 @@ internal static bool TryDeserialize(Stream stream, out Exception result, ILogger catch (Exception e) { // swallowing the exception - logger?.LogWarning("RemoteException", "DeSerialization failed : Reason {0}", e); + logger?.LogWarning("RemoteException: DeSerialization failed : Reason {0}", e); } result = null; diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 1c0e0bdfb..f2ac963df 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index c119b2d3e..a50133677 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 59a38a3e4..45dd168fe 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net6 diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index c1cad41e1..79f7f59ae 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -352,14 +352,10 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth // // This approach avoids some common pitfalls that could lead to undesired encoding. var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path)) - { - Properties = - { - { AppIdKey, appId }, - { MethodNameKey, methodName }, - } - }; + var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path)); + + request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); + request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); if (this.apiTokenHeader is not null) { @@ -399,8 +395,8 @@ public override async Task InvokeMethodWithResponseAsync(Ht { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -423,8 +419,8 @@ public async override Task InvokeMethodAsync(HttpRequestMessage request, Cancell { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -447,8 +443,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -465,8 +461,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe { // Our code path for creating requests places these keys in the request properties. We don't want to fail // if they are not present. - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, @@ -476,8 +472,8 @@ public async override Task InvokeMethodAsync(HttpRequestMe } catch (JsonException ex) { - request.Properties.TryGetValue(AppIdKey, out var appId); - request.Properties.TryGetValue(MethodNameKey, out var methodName); + request.Options.TryGetValue(new HttpRequestOptionsKey(AppIdKey), out var appId); + request.Options.TryGetValue(new HttpRequestOptionsKey(MethodNameKey), out var methodName); throw new InvocationException( appId: appId as string, diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index b2f74c7f9..1e9000c4d 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -117,7 +117,7 @@ protected override async Task SendAsync(HttpRequestMessage } // Internal for testing - internal bool TryRewriteUri(Uri uri, [NotNullWhen(true)] out Uri? rewritten) + internal bool TryRewriteUri(Uri? uri, [NotNullWhen(true)] out Uri? rewritten) { // For now the only invalid cases are when the request URI is missing or just silly. // We may support additional cases for validation in the future (like an allow-list of App-Ids). diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 7eec7f4f7..0f1f64048 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6 enable diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index f6c28b02f..781e891d0 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -2,7 +2,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj index a518db3f5..af0bfa13e 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj index 0f2d6b580..1b8590d7e 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj index 071b72746..962963ac2 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 Dapr.Actors.AspNetCore diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 0ac27e077..8a0fa16d7 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 Dapr.Actors $(DefineConstants);ACTORS diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj index 86a561f59..0e15b6855 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index f3c08b1b7..c3cfd2cbf 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index b153287b2..27f11a308 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index 36f9e725b..c6de11e31 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj index a0f13978d..0455d6db4 100644 --- a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj +++ b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj b/test/Dapr.E2E.Test.App.Grpc/Dapr.E2E.Test.App.Grpc.csproj index abd821d39..8dcf009bc 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 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj index b533b52f3..dd087aef1 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj index 7e114e8df..30d47e7c2 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index 10ae69b38..8b1448759 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index ee842f27b..0d89be674 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -128,15 +128,7 @@ private static string GetTargetFrameworkName() { var targetFrameworkName = ((TargetFrameworkAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false).FirstOrDefault()).FrameworkName; string frameworkMoniker; - if (targetFrameworkName == ".NETCoreApp,Version=v3.1") - { - frameworkMoniker = "netcoreapp3.1"; - } - else if (targetFrameworkName == ".NETCoreApp,Version=v5.0") - { - frameworkMoniker = "net5"; - } - else if (targetFrameworkName == ".NETCoreApp,Version=v6.0") + if (targetFrameworkName == ".NETCoreApp,Version=v6.0") { frameworkMoniker = "net6"; } diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 79e980ccb..0cd57ca67 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1;net6;net7 + net6;net7 From fd7168fdfc47b49de0c5fd102291fd628536add5 Mon Sep 17 00:00:00 2001 From: Erik O'Leary <969938+onionhammer@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:16:45 -0500 Subject: [PATCH 21/84] Proof-of-concept serialization of advanced JSON types, records (#1073) * Add system text json support for actor serialization Signed-off-by: Erik O'Leary * Remove unnecessary stream; directly use serializetobytes Signed-off-by: Erik O'Leary * Disable parallel test execution to make test results more repeatable/predictable Signed-off-by: Erik O'Leary --------- Signed-off-by: Erik O'Leary --- src/Dapr.Actors/Client/ActorProxyFactory.cs | 11 +- src/Dapr.Actors/Client/ActorProxyOptions.cs | 5 + ...geBodyDataContractSerializationProvider.cs | 17 +- .../ActorMessageBodyJsonConverter.cs | 95 ++++++++++ ...torMessageBodyJsonSerializationProvider.cs | 175 ++++++++++++++++++ .../IActorRequestMessageBodySerializer.cs | 3 +- .../IActorResponseMessageBodySerializer.cs | 3 +- src/Dapr.Actors/DaprHttpInteractor.cs | 18 +- src/Dapr.Actors/Runtime/ActorManager.cs | 15 +- src/Dapr.Actors/Runtime/ActorRuntime.cs | 1 + .../Runtime/ActorRuntimeOptions.cs | 20 +- .../Runtime/DataContractStateSerializer.cs | 82 -------- .../Runtime/ActorManagerTests.cs | 4 +- .../ISerializationActor.cs | 22 +++ .../Actors/SerializationActor.cs | 26 +++ test/Dapr.E2E.Test.App/Startup.cs | 6 + .../Actors/E2ETests.CustomSerializerTests.cs | 88 +++++++++ test/Dapr.E2E.Test/DaprRunConfiguration.cs | 6 +- test/Dapr.E2E.Test/DaprTestApp.cs | 5 + test/Dapr.E2E.Test/E2ETests.cs | 4 +- 20 files changed, 494 insertions(+), 112 deletions(-) create mode 100644 src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs create mode 100644 src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs delete mode 100644 src/Dapr.Actors/Runtime/DataContractStateSerializer.cs create mode 100644 test/Dapr.E2E.Test.Actors/ISerializationActor.cs create mode 100644 test/Dapr.E2E.Test.App/Actors/SerializationActor.cs create mode 100644 test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs diff --git a/src/Dapr.Actors/Client/ActorProxyFactory.cs b/src/Dapr.Actors/Client/ActorProxyFactory.cs index 9fd5edddb..4a8fe3a08 100644 --- a/src/Dapr.Actors/Client/ActorProxyFactory.cs +++ b/src/Dapr.Actors/Client/ActorProxyFactory.cs @@ -16,6 +16,7 @@ namespace Dapr.Actors.Client using System; using System.Net.Http; using Dapr.Actors.Builder; + using Dapr.Actors.Communication; using Dapr.Actors.Communication.Client; /// @@ -79,7 +80,15 @@ public object CreateActorProxy(ActorId actorId, Type actorInterfaceType, string options ??= this.DefaultOptions; var daprInteractor = new DaprHttpInteractor(this.handler, options.HttpEndpoint, options.DaprApiToken, options.RequestTimeout); - var remotingClient = new ActorRemotingClient(daprInteractor); + + // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. + IActorMessageBodySerializationProvider serializationProvider = null; + if (options.UseJsonSerialization) + { + serializationProvider = new ActorMessageBodyJsonSerializationProvider(options.JsonSerializerOptions); + } + + var remotingClient = new ActorRemotingClient(daprInteractor, serializationProvider); var proxyGenerator = ActorCodeBuilder.GetOrCreateProxyGenerator(actorInterfaceType); var actorProxy = proxyGenerator.CreateActorProxy(); actorProxy.Initialize(remotingClient, actorId, actorType, options); diff --git a/src/Dapr.Actors/Client/ActorProxyOptions.cs b/src/Dapr.Actors/Client/ActorProxyOptions.cs index 808605c70..665a1dced 100644 --- a/src/Dapr.Actors/Client/ActorProxyOptions.cs +++ b/src/Dapr.Actors/Client/ActorProxyOptions.cs @@ -62,5 +62,10 @@ public JsonSerializerOptions JsonSerializerOptions /// The timeout allowed for an actor request. Can be set to System.Threading.Timeout.InfiniteTimeSpan to disable any timeouts. /// public TimeSpan? RequestTimeout { get; set; } = null; + + /// + /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. + /// + public bool UseJsonSerialization { get; set; } } } diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs index e1991df26..cf16ee2d8 100644 --- a/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs +++ b/src/Dapr.Actors/Communication/ActorMessageBodyDataContractSerializationProvider.cs @@ -17,6 +17,7 @@ namespace Dapr.Actors.Communication using System.Collections.Generic; using System.IO; using System.Runtime.Serialization; + using System.Threading.Tasks; using System.Xml; /// @@ -185,21 +186,21 @@ byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody act return stream.ToArray(); } - IActorRequestMessageBody IActorRequestMessageBodySerializer.Deserialize(Stream stream) + ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) { if (stream == null) { - return null; + return default; } if (stream.Length == 0) { - return null; + return default; } stream.Position = 0; using var reader = this.CreateXmlDictionaryReader(stream); - return (TRequest)this.serializer.ReadObject(reader); + return new ValueTask((TRequest)this.serializer.ReadObject(reader)); } byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) @@ -217,11 +218,11 @@ byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody a return stream.ToArray(); } - IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream messageBody) + ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) { if (messageBody == null) { - return null; + return default; } // TODO check performance @@ -231,11 +232,11 @@ IActorResponseMessageBody IActorResponseMessageBodySerializer.Deserialize(Stream if (stream.Capacity == 0) { - return null; + return default; } using var reader = this.CreateXmlDictionaryReader(stream); - return (TResponse)this.serializer.ReadObject(reader); + return new ValueTask((TResponse)this.serializer.ReadObject(reader)); } /// diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs new file mode 100644 index 000000000..0c77adb10 --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonConverter.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Dapr.Actors.Communication +{ + internal class ActorMessageBodyJsonConverter : JsonConverter + { + private readonly List methodRequestParameterTypes; + private readonly List wrappedRequestMessageTypes; + private readonly Type wrapperMessageType; + + public ActorMessageBodyJsonConverter( + List methodRequestParameterTypes, + List wrappedRequestMessageTypes = null + ) + { + this.methodRequestParameterTypes = methodRequestParameterTypes; + this.wrappedRequestMessageTypes = wrappedRequestMessageTypes; + + if (this.wrappedRequestMessageTypes != null && this.wrappedRequestMessageTypes.Count == 1) + { + this.wrapperMessageType = this.wrappedRequestMessageTypes[0]; + } + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Ensure start-of-object, then advance + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); + reader.Read(); + + // Ensure property name, then advance + if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "value") throw new JsonException(); + reader.Read(); + + // If the value is null, return null. + if (reader.TokenType == JsonTokenType.Null) + { + // Read the end object token. + reader.Read(); + return default; + } + + // If the value is an object, deserialize it to wrapper message type + if (this.wrapperMessageType != null) + { + var value = JsonSerializer.Deserialize(ref reader, this.wrapperMessageType, options); + + // Construct a new WrappedMessageBody with the deserialized value. + var wrapper = new WrappedMessageBody() + { + Value = value, + }; + + // Read the end object token. + reader.Read(); + + // Coerce the type to T; required because WrappedMessageBody inherits from two separate interfaces, which + // cannot both be used as generic constraints + return (T)(object)wrapper; + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("value"); + + if (value is WrappedMessageBody body) + { + JsonSerializer.Serialize(writer, body.Value, body.Value.GetType(), options); + } + else + writer.WriteNullValue(); + writer.WriteEndObject(); + } + } +} diff --git a/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs new file mode 100644 index 000000000..fd35db8e1 --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorMessageBodyJsonSerializationProvider.cs @@ -0,0 +1,175 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Communication +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text.Json; + using System.Threading.Tasks; + using System.Xml; + + /// + /// This is the implmentation for used by remoting service and client during + /// request/response serialization . It uses request Wrapping and data contract for serialization. + /// + internal class ActorMessageBodyJsonSerializationProvider : IActorMessageBodySerializationProvider + { + public JsonSerializerOptions Options { get; } + + /// + /// Initializes a new instance of the class. + /// + public ActorMessageBodyJsonSerializationProvider(JsonSerializerOptions options) + { + Options = options; + } + + /// + /// Creates a MessageFactory for Wrapped Message Json Remoting Types. This is used to create Remoting Request/Response objects. + /// + /// + /// that provides an instance of the factory for creating + /// remoting request and response message bodies. + /// + public IActorMessageBodyFactory CreateMessageBodyFactory() + { + return new WrappedRequestMessageFactory(); + } + + /// + /// Creates IActorRequestMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. + /// + /// The remoted service interface. + /// The union of parameter types of all of the methods of the specified interface. + /// Wrapped Request Types for all Methods. + /// + /// An instance of the that can serialize the service + /// actor request message body to a messaging body for transferring over the transport. + /// + public IActorRequestMessageBodySerializer CreateRequestMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodRequestParameterTypes, + IEnumerable wrappedRequestMessageTypes = null) + { + return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodRequestParameterTypes, wrappedRequestMessageTypes); + } + + /// + /// Creates IActorResponseMessageBodySerializer for a serviceInterface using Wrapped Message Json implementation. + /// + /// The remoted service interface. + /// The return types of all of the methods of the specified interface. + /// Wrapped Response Types for all remoting methods. + /// + /// An instance of the that can serialize the service + /// actor response message body to a messaging body for transferring over the transport. + /// + public IActorResponseMessageBodySerializer CreateResponseMessageBodySerializer( + Type serviceInterfaceType, + IEnumerable methodReturnTypes, + IEnumerable wrappedResponseMessageTypes = null) + { + return new MemoryStreamMessageBodySerializer(Options, serviceInterfaceType, methodReturnTypes, wrappedResponseMessageTypes); + } + + /// + /// Default serializer for service remoting request and response message body that uses the + /// memory stream to create outgoing message buffers. + /// + private class MemoryStreamMessageBodySerializer : + IActorRequestMessageBodySerializer, + IActorResponseMessageBodySerializer + where TRequest : IActorRequestMessageBody + where TResponse : IActorResponseMessageBody + { + private readonly JsonSerializerOptions serializerOptions; + + public MemoryStreamMessageBodySerializer( + JsonSerializerOptions serializerOptions, + Type serviceInterfaceType, + IEnumerable methodRequestParameterTypes, + IEnumerable wrappedRequestMessageTypes = null) + { + var _methodRequestParameterTypes = new List(methodRequestParameterTypes); + var _wrappedRequestMessageTypes = new List(wrappedRequestMessageTypes); + + this.serializerOptions = new(serializerOptions) + { + // Workaround since WrappedMessageBody creates an object + // with parameters as fields + IncludeFields = true, + }; + + this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); + this.serializerOptions.Converters.Add(new ActorMessageBodyJsonConverter(_methodRequestParameterTypes, _wrappedRequestMessageTypes)); + } + + byte[] IActorRequestMessageBodySerializer.Serialize(IActorRequestMessageBody actorRequestMessageBody) + { + if (actorRequestMessageBody == null) + { + return null; + } + + return JsonSerializer.SerializeToUtf8Bytes(actorRequestMessageBody, this.serializerOptions); + } + + async ValueTask IActorRequestMessageBodySerializer.DeserializeAsync(Stream stream) + { + if (stream == null) + { + return default; + } + + if (stream.Length == 0) + { + return default; + } + + stream.Position = 0; + return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + } + + byte[] IActorResponseMessageBodySerializer.Serialize(IActorResponseMessageBody actorResponseMessageBody) + { + if (actorResponseMessageBody == null) + { + return null; + } + + return JsonSerializer.SerializeToUtf8Bytes(actorResponseMessageBody, this.serializerOptions); + } + + async ValueTask IActorResponseMessageBodySerializer.DeserializeAsync(Stream messageBody) + { + if (messageBody == null) + { + return null; + } + + using var stream = new MemoryStream(); + messageBody.CopyTo(stream); + stream.Position = 0; + + if (stream.Capacity == 0) + { + return null; + } + + return await JsonSerializer.DeserializeAsync(stream, this.serializerOptions); + } + } + } +} diff --git a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs index 66c6abb82..b58257381 100644 --- a/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorRequestMessageBodySerializer.cs @@ -14,6 +14,7 @@ namespace Dapr.Actors.Communication { using System.IO; + using System.Threading.Tasks; /// /// Defines the interface that must be implemented to provide a serializer/deserializer for remoting request message body. @@ -32,6 +33,6 @@ internal interface IActorRequestMessageBodySerializer /// /// Serialized message body. /// Deserialized remoting request message body object. - IActorRequestMessageBody Deserialize(Stream messageBody); + ValueTask DeserializeAsync(Stream messageBody); } } diff --git a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs index 2c31019ba..b54191f91 100644 --- a/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs +++ b/src/Dapr.Actors/Communication/IActorResponseMessageBodySerializer.cs @@ -14,6 +14,7 @@ namespace Dapr.Actors.Communication { using System.IO; + using System.Threading.Tasks; /// /// Defines the interface that must be implemented to provide a serializer/deserializer for actor response message body. @@ -32,6 +33,6 @@ internal interface IActorResponseMessageBodySerializer /// /// Serialized message body. /// Deserialized actor response message body object. - IActorResponseMessageBody Deserialize(Stream messageBody); + ValueTask DeserializeAsync(Stream messageBody); } } diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index 410925dae..4695375fb 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ HttpRequestMessage RequestFunc() return request; } - var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); + using var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); var stringResponse = await response.Content.ReadAsStringAsync(); return stringResponse; } @@ -164,11 +164,11 @@ HttpRequestMessage RequestFunc() // actorResponseMessageHeader is not null, it means there is remote exception if (actorResponseMessageHeader != null) { - var isDeserialzied = + var isDeserialized = ActorInvokeException.ToException( responseMessageBody, out var remoteMethodException); - if (isDeserialzied) + if (isDeserialized) { var exceptionDetails = GetExceptionDetails(header.ToString()); throw new ActorMethodInvocationException( @@ -185,7 +185,7 @@ HttpRequestMessage RequestFunc() } } - actorResponseMessageBody = responseBodySerializer.Deserialize(responseMessageBody); + actorResponseMessageBody = await responseBodySerializer.DeserializeAsync(responseMessageBody); } return new ActorResponseMessage(actorResponseMessageHeader, actorResponseMessageBody); @@ -231,8 +231,8 @@ HttpRequestMessage RequestFunc() } var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); - var byteArray = await response.Content.ReadAsStreamAsync(); - return byteArray; + var stream = await response.Content.ReadAsStreamAsync(); + return stream; } public Task RegisterReminderAsync(string actorType, string actorId, string reminderName, string data, CancellationToken cancellationToken = default) @@ -351,7 +351,7 @@ internal async Task SendAsyncGetResponseAsRawJson( string relativeUri, CancellationToken cancellationToken) { - var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); + using var response = await this.SendAsyncHandleUnsuccessfulResponse(requestFunc, relativeUri, cancellationToken); var retValue = default(string); if (response != null && response.Content != null) @@ -458,7 +458,7 @@ private async Task SendAsyncHandleSecurityExceptions( HttpResponseMessage response; // Get the request using the Func as same request cannot be resent when retries are implemented. - var request = requestFunc.Invoke(); + using var request = requestFunc.Invoke(); // add token for dapr api token based authentication this.AddDaprApiTokenHeader(request); diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index 6278b0ded..b7ee3bf3e 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -56,7 +56,8 @@ internal sealed class ActorManager internal ActorManager( ActorRegistration registration, ActorActivator activator, - JsonSerializerOptions jsonSerializerOptions, + JsonSerializerOptions jsonSerializerOptions, + bool useJsonSerialization, ILoggerFactory loggerFactory, IActorProxyFactory proxyFactory, IDaprInteractor daprInteractor) @@ -78,7 +79,15 @@ internal ActorManager( this.activeActors = new ConcurrentDictionary(); this.reminderMethodContext = ActorMethodContext.CreateForReminder(ReceiveReminderMethodName); this.timerMethodContext = ActorMethodContext.CreateForTimer(TimerMethodName); - this.serializersManager = IntializeSerializationManager(null); + + // provide a serializer if 'useJsonSerialization' is true and no serialization provider is provided. + IActorMessageBodySerializationProvider serializationProvider = null; + if (useJsonSerialization) + { + serializationProvider = new ActorMessageBodyJsonSerializationProvider(jsonSerializerOptions); + } + + this.serializersManager = IntializeSerializationManager(serializationProvider); this.messageBodyFactory = new WrappedRequestMessageFactory(); this.logger = loggerFactory.CreateLogger(this.GetType()); @@ -103,7 +112,7 @@ internal async Task> DispatchWithRemotingAsync(ActorId act using (var stream = new MemoryStream()) { await data.CopyToAsync(stream); - actorMessageBody = msgBodySerializer.Deserialize(stream); + actorMessageBody = await msgBodySerializer.DeserializeAsync(stream); } // Call the method on the method dispatcher using the Func below. diff --git a/src/Dapr.Actors/Runtime/ActorRuntime.cs b/src/Dapr.Actors/Runtime/ActorRuntime.cs index 8d2ae0cab..7f01fefb7 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntime.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntime.cs @@ -55,6 +55,7 @@ internal ActorRuntime(ActorRuntimeOptions options, ILoggerFactory loggerFactory, actor, actor.Activator ?? this.activatorFactory.CreateActivator(actor.Type), this.options.JsonSerializerOptions, + this.options.UseJsonSerialization, loggerFactory, proxyFactory, daprInteractor); diff --git a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs index 30ee7b476..62eaceea6 100644 --- a/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs +++ b/src/Dapr.Actors/Runtime/ActorRuntimeOptions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ public sealed class ActorRuntimeOptions { Enabled = false, }; + private bool useJsonSerialization = false; private JsonSerializerOptions jsonSerializerOptions = JsonSerializerDefaults.Web; private string daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); private int? remindersStoragePartitions = null; @@ -151,7 +152,22 @@ public ActorReentrancyConfig ReentrancyConfig } } - + /// + /// Enable JSON serialization for actor proxy message serialization in both remoting and non-remoting invocations. + /// + public bool UseJsonSerialization + { + get + { + return this.useJsonSerialization; + } + + set + { + this.useJsonSerialization = value; + } + } + /// /// The to use for actor state persistence and message deserialization /// diff --git a/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs b/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs deleted file mode 100644 index e30b21d48..000000000 --- a/src/Dapr.Actors/Runtime/DataContractStateSerializer.cs +++ /dev/null @@ -1,82 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -namespace Dapr.Actors.Runtime -{ - using System; - using System.Collections.Concurrent; - using System.IO; - using System.Runtime.Serialization; - using System.Xml; - - /// - /// DataContract serializer for Actor state serialization/deserialziation. - /// This is the default state serializer used with Service Fabric Reliable Actors. - /// If there is user ask for the compatibility, this can be exposed by adding a compatibility option as an attribute on Actor type so that Service Fabric Reliable Actors state serialization behavior - /// can also be used using Dapr. - /// - internal class DataContractStateSerializer : IActorStateSerializer - { - private readonly ConcurrentDictionary actorStateSerializerCache; - - internal DataContractStateSerializer() - { - this.actorStateSerializerCache = new ConcurrentDictionary(); - } - - public byte[] Serialize(Type stateType, T state) - { - var serializer = this.actorStateSerializerCache.GetOrAdd( - stateType, - CreateDataContractSerializer); - - using var stream = new MemoryStream(); - using var writer = XmlDictionaryWriter.CreateBinaryWriter(stream); - serializer.WriteObject(writer, state); - writer.Flush(); - return stream.ToArray(); - } - - public T Deserialize(byte[] buffer) - { - if ((buffer == null) || (buffer.Length == 0)) - { - return default; - } - - var serializer = this.actorStateSerializerCache.GetOrAdd( - typeof(T), - CreateDataContractSerializer); - - using var stream = new MemoryStream(buffer); - using var reader = XmlDictionaryReader.CreateBinaryReader(stream, XmlDictionaryReaderQuotas.Max); - return (T)serializer.ReadObject(reader); - } - - private static DataContractSerializer CreateDataContractSerializer(Type actorStateType) - { - var dataContractSerializer = new DataContractSerializer( - actorStateType, - new DataContractSerializerSettings - { - MaxItemsInObjectGraph = int.MaxValue, - KnownTypes = new[] - { - typeof(ActorReference), - }, - }); - - return dataContractSerializer; - } - } -} diff --git a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs index dd3683124..6b92c7e18 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorManagerTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ private ActorManager CreateActorManager(Type type, ActorActivator activator = nu { var registration = new ActorRegistration(ActorTypeInformation.Get(type, actorTypeName: null)); var interactor = new DaprHttpInteractor(clientHandler: null, "http://localhost:3500", apiToken: null, requestTimeout: null); - return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); + return new ActorManager(registration, activator ?? new DefaultActorActivator(), JsonSerializerDefaults.Web, false, NullLoggerFactory.Instance, ActorProxy.DefaultProxyFactory, interactor); } [Fact] diff --git a/test/Dapr.E2E.Test.Actors/ISerializationActor.cs b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs new file mode 100644 index 000000000..28190a0d7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/ISerializationActor.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors +{ + public interface ISerializationActor : IActor, IPingActor + { + Task SendAsync(string name, SerializationPayload payload, CancellationToken cancellationToken = default); + } + + public record SerializationPayload(string Message) + { + public JsonElement Value { get; set; } + + [JsonExtensionData] + public Dictionary ExtensionData { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs new file mode 100644 index 000000000..e8da59826 --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/SerializationActor.cs @@ -0,0 +1,26 @@ + +using System.Threading; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Serialization +{ + public class SerializationActor : Actor, ISerializationActor + { + public SerializationActor(ActorHost host) + : base(host) + { + } + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task SendAsync(string name, + SerializationPayload payload, CancellationToken cancellationToken = default) + { + return Task.FromResult(payload); + } + } +} diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index b5aebbba2..34e1b2eb8 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -17,6 +17,7 @@ namespace Dapr.E2E.Test using Dapr.E2E.Test.Actors.Reminders; using Dapr.E2E.Test.Actors.Timers; using Dapr.E2E.Test.Actors.ExceptionTesting; + using Dapr.E2E.Test.Actors.Serialization; using Dapr.E2E.Test.App.ErrorTesting; using Dapr.Workflow; using Microsoft.AspNetCore.Authentication; @@ -34,6 +35,9 @@ namespace Dapr.E2E.Test /// public class Startup { + bool JsonSerializationEnabled => + System.Linq.Enumerable.Contains(System.Environment.GetCommandLineArgs(), "--json-serialization"); + /// /// Initializes a new instance of the class. /// @@ -83,10 +87,12 @@ public void ConfigureServices(IServiceCollection services) }); services.AddActors(options => { + options.UseJsonSerialization = JsonSerializationEnabled; options.Actors.RegisterActor(); options.Actors.RegisterActor(); options.Actors.RegisterActor(); options.Actors.RegisterActor(); + options.Actors.RegisterActor(); }); } diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs new file mode 100644 index 000000000..c393f2ef1 --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.CustomSerializerTests.cs @@ -0,0 +1,88 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +namespace Dapr.E2E.Test +{ + using System; + using System.Diagnostics; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Dapr.Actors; + using Dapr.Actors.Client; + using Dapr.E2E.Test.Actors; + using Xunit; + using Xunit.Abstractions; + + public class CustomSerializerTests : DaprTestAppLifecycle + { + private readonly Lazy proxyFactory; + private IActorProxyFactory ProxyFactory => this.HttpEndpoint == null ? null : this.proxyFactory.Value; + + public CustomSerializerTests(ITestOutputHelper output, DaprTestAppFixture fixture) : base(output, fixture) + { + base.Configuration = new DaprRunConfiguration + { + UseAppPort = true, + AppId = "serializerapp", + AppJsonSerialization = true, + TargetProject = "./../../../../../test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj" + }; + + this.proxyFactory = new Lazy(() => + { + Debug.Assert(this.HttpEndpoint != null); + return new ActorProxyFactory(new ActorProxyOptions() { + HttpEndpoint = this.HttpEndpoint, + JsonSerializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + }, + UseJsonSerialization = true, + }); + }); + } + + [Fact] + public async Task ActorCanSupportCustomSerializer() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "SerializationActor"); + + await ActorRuntimeChecker.WaitForActorRuntimeAsync(this.AppId, this.Output, proxy, cts.Token); + + var payload = new SerializationPayload("hello world") + { + Value = JsonSerializer.SerializeToElement(new { foo = "bar" }), + ExtensionData = new System.Collections.Generic.Dictionary() + { + { "baz", "qux" }, + { "count", 42 }, + } + }; + + var result = await proxy.SendAsync("test", payload, CancellationToken.None); + + Assert.Equal(payload.Message, result.Message); + Assert.Equal(payload.Value.GetRawText(), result.Value.GetRawText()); + Assert.Equal(payload.ExtensionData.Count, result.ExtensionData.Count); + + foreach (var kvp in payload.ExtensionData) + { + Assert.True(result.ExtensionData.TryGetValue(kvp.Key, out var value)); + Assert.Equal(JsonSerializer.Serialize(kvp.Value), JsonSerializer.Serialize(value)); + } + } + } +} diff --git a/test/Dapr.E2E.Test/DaprRunConfiguration.cs b/test/Dapr.E2E.Test/DaprRunConfiguration.cs index 9e423205d..fccfbcdd4 100644 --- a/test/Dapr.E2E.Test/DaprRunConfiguration.cs +++ b/test/Dapr.E2E.Test/DaprRunConfiguration.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ public class DaprRunConfiguration public string AppId { get; set; } public string AppProtocol { get; set; } + + public bool AppJsonSerialization { get; set; } public string ConfigurationPath { get; set; } public string TargetProject { get; set; } } -} \ No newline at end of file +} diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index 0d89be674..d65e21fd6 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -89,6 +89,11 @@ public DaprTestApp(ITestOutputHelper output, string appId) arguments.AddRange(new[] { "--urls", $"http://localhost:{appPort.ToString(CultureInfo.InvariantCulture)}", }); } + if (configuration.AppJsonSerialization) + { + arguments.AddRange(new[] { "--json-serialization" }); + } + // TODO: we don't do any quoting right now because our paths are guaranteed not to contain spaces var daprStart = new DaprCommand(this.testOutput) { diff --git a/test/Dapr.E2E.Test/E2ETests.cs b/test/Dapr.E2E.Test/E2ETests.cs index 94ebbf3df..bc469f715 100644 --- a/test/Dapr.E2E.Test/E2ETests.cs +++ b/test/Dapr.E2E.Test/E2ETests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ using Xunit; using Xunit.Abstractions; +[assembly: CollectionBehavior(DisableTestParallelization = true)] + namespace Dapr.E2E.Test { // We're using IClassFixture to manage the state we need across tests. From 87329f62b1f6bf09765ce4520fa1e5a7f7c26b99 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri <67934986+RyanLettieri@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:51:32 -0600 Subject: [PATCH 22/84] Updating workflow collection to allow for use of API Token validation (#1141) Updating workflow collection to allow for use of API Token validation Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 4 + .../Workflow/WorkflowConsoleApp/Program.cs | 229 +++++++++--------- src/Dapr.Workflow/Dapr.Workflow.csproj | 4 + .../WorkflowServiceCollectionExtensions.cs | 63 ++++- src/Shared/DaprDefaults.cs | 20 +- 5 files changed, 196 insertions(+), 124 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index b93af1bb5..e119465c3 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -9,6 +9,10 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) + +## Optional Setup +Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/docs/api-token.md) + ## Projects in sample This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index a1189e70b..9aae2427e 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -47,7 +47,16 @@ using var host = builder.Build(); host.Start(); -using var daprClient = new DaprClientBuilder().Build(); +DaprClient daprClient; +string apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); +if (!string.IsNullOrEmpty(apiToken)) +{ + daprClient = new DaprClientBuilder().UseDaprApiToken(apiToken).Build(); +} +else +{ + daprClient = new DaprClientBuilder().Build(); +} // Wait for the sidecar to become available while (!await daprClient.CheckHealthAsync()) @@ -70,136 +79,138 @@ await RestockInventory(daprClient, baseInventory); // Start the input loop -while (true) +using (daprClient) { - // Get the name of the item to order and make sure we have inventory - string items = string.Join(", ", baseInventory.Select(i => i.Name)); - Console.WriteLine($"Enter the name of one of the following items to order [{items}]."); - Console.WriteLine("To restock items, type 'restock'."); - string itemName = Console.ReadLine()?.Trim(); - if (string.IsNullOrEmpty(itemName)) - { - continue; - } - else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase)) + while (true) { - await RestockInventory(daprClient, baseInventory); - continue; - } + // Get the name of the item to order and make sure we have inventory + string items = string.Join(", ", baseInventory.Select(i => i.Name)); + Console.WriteLine($"Enter the name of one of the following items to order [{items}]."); + Console.WriteLine("To restock items, type 'restock'."); + string itemName = Console.ReadLine()?.Trim(); + if (string.IsNullOrEmpty(itemName)) + { + continue; + } + else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase)) + { + await RestockInventory(daprClient, baseInventory); + continue; + } - InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase)); - if (item == null) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"We don't have {itemName}!"); - Console.ResetColor(); - continue; - } + InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase)); + if (item == null) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"We don't have {itemName}!"); + Console.ResetColor(); + continue; + } - Console.WriteLine($"How many {itemName} would you like to purchase?"); - string amountStr = Console.ReadLine().Trim(); - if (!int.TryParse(amountStr, out int amount) || amount <= 0) - { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"Invalid input. Assuming you meant to type '1'."); - Console.ResetColor(); - amount = 1; - } + Console.WriteLine($"How many {itemName} would you like to purchase?"); + string amountStr = Console.ReadLine().Trim(); + if (!int.TryParse(amountStr, out int amount) || amount <= 0) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Invalid input. Assuming you meant to type '1'."); + Console.ResetColor(); + amount = 1; + } - // Construct the order with a unique order ID - string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; - double totalCost = amount * item.PerItemCost; - var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount); + // Construct the order with a unique order ID + string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; + double totalCost = amount * item.PerItemCost; + var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount); - // Start the workflow using the order ID as the workflow ID - Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); - await daprClient.StartWorkflowAsync( - workflowComponent: DaprWorkflowComponent, - workflowName: nameof(OrderProcessingWorkflow), - input: orderInfo, - instanceId: orderId); + // Start the workflow using the order ID as the workflow ID + Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); + await daprClient.StartWorkflowAsync( + workflowComponent: DaprWorkflowComponent, + workflowName: nameof(OrderProcessingWorkflow), + input: orderInfo, + instanceId: orderId); - // Wait for the workflow to start and confirm the input - GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); + // Wait for the workflow to start and confirm the input + GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent); - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); - // Wait for the workflow to complete - while (true) - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - try + // Wait for the workflow to complete + while (true) { - state = await daprClient.WaitForWorkflowCompletionAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - cancellationToken: cts.Token); - break; - } - catch (OperationCanceledException) - { - // Check to see if the workflow is blocked waiting for an approval - state = await daprClient.GetWorkflowAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); - if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && - customStatus.Contains("Waiting for approval")) + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try { - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); - string approval = Console.ReadLine(); - ApprovalResult approvalResult = ApprovalResult.Unspecified; - if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Approving order..."); - approvalResult = ApprovalResult.Approved; - } - else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Rejecting order..."); - approvalResult = ApprovalResult.Rejected; - } - - if (approvalResult != ApprovalResult.Unspecified) + state = await daprClient.WaitForWorkflowCompletionAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + cancellationToken: cts.Token); + break; + } + catch (OperationCanceledException) + { + // Check to see if the workflow is blocked waiting for an approval + state = await daprClient.GetWorkflowAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent); + if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && + customStatus.Contains("Waiting for approval")) { - // Raise the workflow event to the workflow - await daprClient.RaiseWorkflowEventAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - eventName: "ManagerApproval", - eventData: approvalResult); + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); + string approval = Console.ReadLine(); + ApprovalResult approvalResult = ApprovalResult.Unspecified; + if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Approving order..."); + approvalResult = ApprovalResult.Approved; + } + else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Rejecting order..."); + approvalResult = ApprovalResult.Rejected; + } + + if (approvalResult != ApprovalResult.Unspecified) + { + // Raise the workflow event to the workflow + await daprClient.RaiseWorkflowEventAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + eventName: "ManagerApproval", + eventData: approvalResult); + } + + // otherwise, keep waiting } - - // otherwise, keep waiting } } - } - if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) - { - OrderResult result = state.ReadOutputAs(); - if (result.Processed) + if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); - Console.ResetColor(); + OrderResult result = state.ReadOutputAs(); + if (result.Processed) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); + Console.ResetColor(); + } + else + { + Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + } } - else + else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) { - Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"The workflow failed - {state.FailureDetails}"); + Console.ResetColor(); } - } - else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"The workflow failed - {state.FailureDetails}"); - Console.ResetColor(); - } - Console.WriteLine(); + Console.WriteLine(); + } } - static async Task RestockInventory(DaprClient daprClient, List inventory) { Console.WriteLine("*** Restocking inventory..."); diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 781e891d0..9d8ba1a4e 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index c24265475..50880ab24 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -14,16 +14,20 @@ namespace Dapr.Workflow { using System; + using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using System.Net.Http; + using Dapr; /// /// Contains extension methods for using Dapr Workflow with dependency injection. /// public static class WorkflowServiceCollectionExtensions { + /// /// Adds Dapr Workflow support to the service collection. /// @@ -57,7 +61,18 @@ public static IServiceCollection AddDaprWorkflow( if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -85,7 +100,18 @@ public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection s { if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -104,13 +130,13 @@ static bool TryGetGrpcAddress(out string address) // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. - string? daprEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); if (!String.IsNullOrEmpty(daprEndpoint)) { address = daprEndpoint; return true; } - string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + 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 @@ -126,6 +152,33 @@ static bool TryGetGrpcAddress(out string address) address = string.Empty; return false; } + + static GrpcChannel CreateChannel(string address, HttpClient client) + { + + GrpcChannelOptions options = new() { HttpClient = client}; + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + if (!String.IsNullOrEmpty(daprEndpoint)) { + return GrpcChannel.ForAddress(daprEndpoint, options); + } + + var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + if (int.TryParse(daprPortStr, out int daprGrpcPort)) + { + // If there is no address passed in, we default to localhost + if (String.IsNullOrEmpty(address)) + { + // There is a bug in the Durable Task SDK that requires us to change the format of the address + // depending on the version of .NET that we're targeting. For now, we work around this manually. + #if NET6_0_OR_GREATER + address = $"http://localhost:{daprGrpcPort}"; + #else + address = $"localhost:{daprGrpcPort}"; + #endif + } + + } + return GrpcChannel.ForAddress(address, options); + } } } - diff --git a/src/Shared/DaprDefaults.cs b/src/Shared/DaprDefaults.cs index 1ddab49b0..b738de921 100644 --- a/src/Shared/DaprDefaults.cs +++ b/src/Shared/DaprDefaults.cs @@ -17,10 +17,10 @@ namespace Dapr { internal static class DaprDefaults { - private static string httpEndpoint; - private static string grpcEndpoint; - private static string daprApiToken; - private static string appApiToken; + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; /// /// Get the value of environment variable DAPR_API_TOKEN @@ -31,11 +31,11 @@ public static string GetDefaultDaprApiToken() // Lazy-init is safe because this is just populating the default // We don't plan to support the case where the user changes environment variables // for a running process. - if (daprApiToken == null) + if (string.IsNullOrEmpty(daprApiToken)) { // Treat empty the same as null since it's an environment variable var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = (value == string.Empty) ? null : value; + daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return daprApiToken; @@ -47,10 +47,10 @@ public static string GetDefaultDaprApiToken() /// The value of environment variable APP_API_TOKEN public static string GetDefaultAppApiToken() { - if (appApiToken == null) + if (string.IsNullOrEmpty(appApiToken)) { var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = (value == string.Empty) ? null : value; + appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return appApiToken; @@ -62,7 +62,7 @@ public static string GetDefaultAppApiToken() /// The value of HTTP endpoint based off environment variables public static string GetDefaultHttpEndpoint() { - if (httpEndpoint == null) + if (string.IsNullOrEmpty(httpEndpoint)) { var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); if (!string.IsNullOrEmpty(endpoint)) { @@ -84,7 +84,7 @@ public static string GetDefaultHttpEndpoint() /// The value of gRPC endpoint based off environment variables public static string GetDefaultGrpcEndpoint() { - if (grpcEndpoint == null) + if (string.IsNullOrEmpty(grpcEndpoint)) { var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); if (!string.IsNullOrEmpty(endpoint)) { From 99d874a2b138af020df099a0fc0a09a7d0597fae Mon Sep 17 00:00:00 2001 From: Fabian Martinez <46371672+famarting@users.noreply.github.com> Date: Fri, 8 Sep 2023 00:21:11 +0200 Subject: [PATCH 23/84] set dapr-api-token to healthz requests when needed (#1145) Signed-off-by: Fabian Martinez <46371672+famarting@users.noreply.github.com> Co-authored-by: Yaron Schneider --- src/Dapr.Client/DaprClientGrpc.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 79f7f59ae..75df09323 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -1743,6 +1743,12 @@ public override async Task CheckHealthAsync(CancellationToken cancellation { var path = "/v1.0/healthz"; var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); + } + try { using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); @@ -1759,6 +1765,12 @@ public override async Task CheckOutboundHealthAsync(CancellationToken canc { var path = "/v1.0/healthz/outbound"; var request = new HttpRequestMessage(HttpMethod.Get, new Uri(this.httpEndpoint, path)); + + if (this.apiTokenHeader is not null) + { + request.Headers.Add(this.apiTokenHeader.Value.Key, this.apiTokenHeader.Value.Value); + } + try { using var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); From 3b979e6bdb1d779563f1656fa684183b2bfecd08 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri <67934986+RyanLettieri@users.noreply.github.com> Date: Fri, 6 Oct 2023 12:37:38 -0600 Subject: [PATCH 24/84] Adding in new test for parallel raise events in workflow (#1155) * Adding in new test for parallel raise events in workflow Signed-off-by: Ryan Lettieri --- test/Dapr.E2E.Test.App/Startup.cs | 5 +++++ test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 12 ++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 34e1b2eb8..d1f291bf9 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -70,6 +70,11 @@ public void ConfigureServices(IServiceCollection services) var itemToPurchase = input; + // There are 5 of the same event to test that multiple similarly-named events can be raised in parallel + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); + await context.WaitForExternalEventAsync("ChangePurchaseItem"); itemToPurchase = await context.WaitForExternalEventAsync("ChangePurchaseItem"); // In real life there are other steps related to placing an order, like reserving diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index ae30a9151..971d04098 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -93,8 +93,16 @@ public async Task TestWorkflows() input: input, workflowOptions: workflowOptions); - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); + // 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)); From 02ab25e9377b806c4501e3a362428e7f9736b2d8 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Nov 2023 15:20:01 -0800 Subject: [PATCH 25/84] Consolidate C# language version to 10. (#1180) Signed-off-by: Phillip Hoff --- examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj | 1 - examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj | 1 - properties/dapr_managed_netcore.props | 2 +- src/Dapr.Workflow/Dapr.Workflow.csproj | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj index 0c40eea0c..25c03a419 100644 --- a/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj +++ b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj @@ -8,7 +8,6 @@ Exe net6 enable - latest 612,618 diff --git a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj index 9c4a74a17..4ce0c9801 100644 --- a/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj +++ b/examples/Workflow/WorkflowUnitTest/WorkflowUnitTest.csproj @@ -3,7 +3,6 @@ net6.0 enable - 10 false diff --git a/properties/dapr_managed_netcore.props b/properties/dapr_managed_netcore.props index 59bb68c9a..3bafcb50c 100644 --- a/properties/dapr_managed_netcore.props +++ b/properties/dapr_managed_netcore.props @@ -3,7 +3,7 @@ Debug - 9.0 + 10.0 true 4 false diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 9d8ba1a4e..ef9722d36 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -9,7 +9,6 @@ Dapr Workflow SDK for building workflows as code with Dapr 0.3.0 alpha - 10.0 From 2332388155ba40171d782e01075c9055b2668975 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Mon, 13 Nov 2023 15:25:56 -0800 Subject: [PATCH 26/84] Update actor reminder example. (#1179) Signed-off-by: Phillip Hoff Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- examples/Actor/ActorClient/Program.cs | 5 ++--- examples/Actor/DemoActor/DemoActor.cs | 15 ++++++++++++--- examples/Actor/IDemoActor/IDemoActor.cs | 16 +++++++++++++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index aeee28386..5d7f06fb2 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -18,7 +18,6 @@ namespace ActorClient using System.Threading.Tasks; using Dapr.Actors; using Dapr.Actors.Client; - using Dapr.Actors.Communication; using IDemoActorInterface; /// @@ -69,7 +68,7 @@ public static async Task Main(string[] args) } catch (ActorMethodInvocationException ex) { - if (ex.InnerException is NotImplementedException) + if (ex.InnerException is ActorInvokeException invokeEx && invokeEx.ActualExceptionType is "System.NotImplementedException") { Console.WriteLine($"Got Correct Exception from actor method invocation."); } @@ -111,7 +110,7 @@ public static async Task Main(string[] args) await Task.Delay(5000); Console.WriteLine("Getting details of the registered reminder"); reminder = await proxy.GetReminder(); - Console.WriteLine($"Received reminder is {reminder}."); + Console.WriteLine($"Received reminder is {reminder?.ToString() ?? "None"} (expecting None)."); Console.WriteLine("Registering reminder with ttl and repetitions, i.e. reminder stops when either condition is met - The reminder will repeat 2 times."); await proxy.RegisterReminderWithTtlAndRepetitions(TimeSpan.FromSeconds(5), 2); Console.WriteLine("Getting details of the registered reminder"); diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index 0ab633fcd..62c100f79 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -85,9 +85,18 @@ public async Task RegisterReminderWithTtlAndRepetitions(TimeSpan ttl, int repeti await this.RegisterReminderAsync("TestReminder", null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1), repetitions, ttl); } - public async Task GetReminder() - { - return await this.GetReminderAsync("TestReminder"); + public async Task GetReminder() + { + var reminder = await this.GetReminderAsync("TestReminder"); + + return reminder is not null + ? new ActorReminderData + { + Name = reminder.Name, + Period = reminder.Period, + DueTime = reminder.DueTime + } + : null; } public Task UnregisterReminder() diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs index adec6df68..c2926a048 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -100,7 +100,7 @@ public interface IDemoActor : IActor /// /// The name of the reminder. /// A task that returns the reminder after completion. - Task GetReminder(); + Task GetReminder(); /// /// Unregisters the registered timer. @@ -132,4 +132,18 @@ public override string ToString() return $"PropertyA: {propAValue}, PropertyB: {propBValue}"; } } + + public class ActorReminderData + { + public string Name { get; set; } + + public TimeSpan DueTime { get; set; } + + public TimeSpan Period { get; set; } + + public override string ToString() + { + return $"Name: {this.Name}, DueTime: {this.DueTime}, Period: {this.Period}"; + } + } } From 39a38f6e9b57f839cf74d0dbfaf60286106b0a38 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri <67934986+RyanLettieri@users.noreply.github.com> Date: Tue, 14 Nov 2023 13:17:17 -0700 Subject: [PATCH 27/84] Initial implementation for workflow log tracing (#1176) * Initial setup for workflow log tracing Signed-off-by: Ryan Lettieri * Created log sink for E2E tests using serilog Signed-off-by: Ryan Lettieri * Formatting Signed-off-by: Ryan Lettieri * Addressing feedback on review Signed-off-by: Ryan Lettieri * Addressing some review comments Signed-off-by: Ryan Lettieri * Addressing more feedbck in the workflow e2e test Signed-off-by: Ryan Lettieri --------- Signed-off-by: Ryan Lettieri --- src/Dapr.Workflow/WorkflowLoggingService.cs | 75 +++++++++++++++++++ src/Dapr.Workflow/WorkflowRuntimeOptions.cs | 4 + .../WorkflowServiceCollectionExtensions.cs | 2 +- .../Dapr.E2E.Test.App.csproj | 7 ++ test/Dapr.E2E.Test.App/Program.cs | 28 +++---- test/Dapr.E2E.Test.App/Startup.cs | 14 ++++ test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 64 +++++++++++++++- 7 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 src/Dapr.Workflow/WorkflowLoggingService.cs diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs new file mode 100644 index 000000000..482d95b97 --- /dev/null +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------ +// Copyright 2022 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Workflow +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Configuration; + + /// + /// Defines runtime options for workflows. + /// + internal sealed class WorkflowLoggingService : IHostedService + { + private readonly ILogger logger; + private static readonly HashSet registeredWorkflows = new(); + private static readonly HashSet registeredActivities = new(); + + public WorkflowLoggingService(ILogger logger, IConfiguration configuration) + { + this.logger = logger; + + } + public Task StartAsync(CancellationToken cancellationToken) + { + this.logger.Log(LogLevel.Information, "WorkflowLoggingService started"); + + this.logger.Log(LogLevel.Information, "List of registered workflows"); + foreach (string item in registeredWorkflows) + { + this.logger.Log(LogLevel.Information, item); + } + + this.logger.Log(LogLevel.Information, "List of registered activities:"); + foreach (string item in registeredActivities) + { + this.logger.Log(LogLevel.Information, item); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + this.logger.Log(LogLevel.Information, "WorkflowLoggingService stopped"); + + return Task.CompletedTask; + } + + public static void LogWorkflowName(string workflowName) + { + registeredWorkflows.Add(workflowName); + } + + public static void LogActivityName(string activityName) + { + registeredActivities.Add(activityName); + } + + } +} diff --git a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs index 4dd202b1a..adc925777 100644 --- a/src/Dapr.Workflow/WorkflowRuntimeOptions.cs +++ b/src/Dapr.Workflow/WorkflowRuntimeOptions.cs @@ -54,6 +54,7 @@ public void RegisterWorkflow(string name, Func(string name, Func(); return new OrchestratorWrapper(workflow); }); + WorkflowLoggingService.LogWorkflowName(name); }); } @@ -91,6 +93,7 @@ public void RegisterActivity(string name, Func() where TActivity : class, IWorkflowActi TActivity activity = ActivatorUtilities.CreateInstance(serviceProvider); return new ActivityWrapper(activity); }); + WorkflowLoggingService.LogActivityName(name); }); } diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 50880ab24..ca514f221 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -47,7 +47,7 @@ public static IServiceCollection AddDaprWorkflow( #pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient serviceCollection.TryAddSingleton(); #pragma warning restore CS0618 // Type or member is obsolete - + serviceCollection.AddHostedService(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); serviceCollection.AddDaprWorkflowClient(); 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 30d47e7c2..5c81557b8 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -10,4 +10,11 @@ + + + + + + + diff --git a/test/Dapr.E2E.Test.App/Program.cs b/test/Dapr.E2E.Test.App/Program.cs index e617558bf..5713129d5 100644 --- a/test/Dapr.E2E.Test.App/Program.cs +++ b/test/Dapr.E2E.Test.App/Program.cs @@ -1,23 +1,25 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ namespace Dapr.E2E.Test { using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; + using Serilog; public class Program { public static void Main(string[] args) { + Log.Logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); CreateHostBuilder(args).Build().Run(); } @@ -26,6 +28,6 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }).UseSerilog(); } } diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index d1f291bf9..8207c5883 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -29,6 +29,8 @@ namespace Dapr.E2E.Test using Microsoft.Extensions.Hosting; using System.Threading.Tasks; using System; + using Microsoft.Extensions.Logging; + using Serilog; /// /// Startup class. @@ -61,6 +63,10 @@ public void ConfigureServices(IServiceCollection services) services.AddAuthentication().AddDapr(); services.AddAuthorization(o => o.AddDapr()); services.AddControllers().AddDapr(); + services.AddLogging(builder => + { + builder.AddConsole(); + }); // Register a workflow and associated activity services.AddDaprWorkflow(options => { @@ -108,11 +114,19 @@ public void ConfigureServices(IServiceCollection services) /// Webhost environment. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + var logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger(); + + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddSerilog(logger); + }); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + app.UseSerilogRequestLogging(); + app.UseRouting(); app.UseAuthentication(); diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index 971d04098..979c136da 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -11,25 +11,81 @@ // limitations under the License. // ------------------------------------------------------------------------ using System; +using System.IO; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Dapr.Client; using FluentAssertions; using Xunit; +using System.Linq; +using System.Diagnostics; namespace Dapr.E2E.Test { [Obsolete] public partial class E2ETests { + [Fact] + public async Task TestWorkflowLogging() + { + // This test starts the daprclient and searches through the logfile to ensure the + // workflow logger is correctly logging the registered workflow(s) and activity(s) + + Dictionary logStrings = new Dictionary(); + logStrings["PlaceOrder"] = false; + logStrings["ShipProduct"] = false; + var logFilePath = "../../../../../test/Dapr.E2E.Test.App/log.txt"; + var allLogsFound = false; + var timeout = 30; // 30s + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); + using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); + var health = await daprClient.CheckHealthAsync(); + health.Should().Be(true, "DaprClient is not healthy"); + + var searchTask = Task.Run(async() => + { + using (StreamReader reader = new StreamReader(logFilePath)) + { + string line; + while ((line = await reader.ReadLineAsync().WaitAsync(cts.Token)) != null) + { + foreach (var entry in logStrings) + { + if (line.Contains(entry.Key)) + { + logStrings[entry.Key] = true; + } + } + allLogsFound = logStrings.All(k => k.Value); + if (allLogsFound) + { + break; + } + } + } + }, cts.Token); + + try + { + await searchTask; + } + finally + { + File.Delete(logFilePath); + } + if (!allLogsFound) + { + Assert.True(false, "The logs were not able to found within the timeout"); + } + } [Fact] public async Task TestWorkflows() { - string instanceId = "testInstanceId"; - string instanceId2 = "EventRaiseId"; - string workflowComponent = "dapr"; - string workflowName = "PlaceOrder"; + var instanceId = "testInstanceId"; + var instanceId2 = "EventRaiseId"; + var workflowComponent = "dapr"; + var workflowName = "PlaceOrder"; object input = "paperclips"; Dictionary workflowOptions = new Dictionary(); workflowOptions.Add("task_queue", "testQueue"); From 09008bb43b89c85390db297d5bd8379a5c42d795 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 15 Nov 2023 10:48:02 -0800 Subject: [PATCH 28/84] .NET 8 Support (#1188) * Update test projects to use .NET 8. Signed-off-by: Phillip Hoff * Update SDK projects to target .NET 8. Signed-off-by: Phillip Hoff * Update workflows to target .NET 8. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .github/workflows/itests.yml | 13 +++++++++---- .github/workflows/sdk_build.yml | 16 +++++++++++----- .../ActorsEndpointRouteBuilderExtensions.cs | 4 ++-- .../Dapr.Actors.AspNetCore.csproj | 3 --- src/Dapr.Actors/Dapr.Actors.csproj | 4 ---- src/Dapr.AspNetCore/Dapr.AspNetCore.csproj | 4 ---- src/Dapr.AspNetCore/DaprAuthenticationHandler.cs | 9 +++++++++ src/Dapr.Client/Dapr.Client.csproj | 4 ---- src/Dapr.Client/DaprApiException.cs | 6 ++++++ src/Dapr.Client/DaprException.cs | 3 +++ .../Dapr.Extensions.Configuration.csproj | 1 - src/Dapr.Workflow/Dapr.Workflow.csproj | 3 ++- src/Directory.Build.props | 1 + ....Actors.AspNetCore.IntegrationTest.App.csproj | 4 ---- ...Dapr.Actors.AspNetCore.IntegrationTest.csproj | 4 ---- .../Dapr.Actors.AspNetCore.Test.csproj | 1 - test/Dapr.Actors.Test/Dapr.Actors.Test.csproj | 1 - .../Dapr.AspNetCore.IntegrationTest.App.csproj | 4 ---- .../Dapr.AspNetCore.IntegrationTest.csproj | 3 --- .../Dapr.AspNetCore.Test.csproj | 3 --- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 3 --- .../Dapr.E2E.Test.Actors.csproj | 4 ---- .../Dapr.E2E.Test.App.Grpc.csproj | 3 --- .../Dapr.E2E.Test.App.ReentrantActors.csproj | 4 ---- test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj | 4 ---- test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 3 --- test/Dapr.E2E.Test/DaprTestApp.cs | 16 +++++++--------- .../Dapr.Extensions.Configuration.Test.csproj | 3 --- test/Directory.Build.props | 2 ++ 29 files changed, 52 insertions(+), 81 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 870264f40..e4c061428 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['6.0', '7.0'] + dotnet-version: ['6.0', '7.0', '8.0'] include: - dotnet-version: '6.0' display-name: '.NET 6.0' @@ -31,6 +31,11 @@ jobs: framework: 'net7' prefix: 'net7' install-version: '7.0.x' + - dotnet-version: '8.0' + display-name: '.NET 8.0' + framework: 'net8' + prefix: 'net8' + install-version: '8.0.x' env: NUPKG_OUTDIR: bin/Release/nugets GOVER: 1.20.3 @@ -101,11 +106,11 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 7.0 # net7 is always required. + - name: Setup .NET 8.0 # net8 is always required. uses: actions/setup-dotnet@v1 - if: ${{ matrix.install-version != '7.0.x' }} + if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release /p:GITHUB_ACTIONS=false diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 1c4e1a60e..4fde80610 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -26,7 +26,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build run: dotnet build --configuration release - name: Generate Packages @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - dotnet-version: ['6.0', '7.0'] + dotnet-version: ['6.0', '7.0', '8.0'] include: - dotnet-version: '6.0' install-3: false @@ -56,6 +56,12 @@ jobs: framework: 'net7' prefix: 'net7' install-version: '7.0.x' + - dotnet-version: '8.0' + install-3: false + display-name: '.NET 8.0' + framework: 'net8' + prefix: 'net8' + install-version: '8.0.x' steps: - uses: actions/checkout@v1 - name: Parse release version @@ -64,11 +70,11 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.install-version }} - - name: Setup .NET 7.0 # net7 is always required. + - name: Setup .NET 8.0 # net8 is always required. uses: actions/setup-dotnet@v1 - if: ${{ matrix.install-version != '7.0.x' }} + if: ${{ matrix.install-version != '8.0.x' }} with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release /p:GITHUB_ACTIONS=false diff --git a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs index e7937bbbf..55d161d9a 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs @@ -109,7 +109,7 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR if (header != string.Empty) { // exception case - context.Response.Headers.Add(Constants.ErrorResponseHeaderName, header); // add error header + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; // add error header } await context.Response.Body.WriteAsync(body, 0, body.Length); // add response message body @@ -118,7 +118,7 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR { var (header, body) = CreateExceptionResponseMessage(ex); - context.Response.Headers.Add(Constants.ErrorResponseHeaderName, header); + context.Response.Headers[Constants.ErrorResponseHeaderName] = header; await context.Response.Body.WriteAsync(body, 0, body.Length); } finally diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index cfd7d8123..1114b7828 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -1,7 +1,4 @@  - - net6 - This package contains the reference assemblies for developing Actor services using Dapr. diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index a50133677..c61fc5abc 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -1,9 +1,5 @@  - - net6 - - This package contains the reference assemblies for developing services using Dapr and AspNetCore. diff --git a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs index 822694c3b..dc21b5926 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationHandler.cs @@ -25,6 +25,14 @@ internal class DaprAuthenticationHandler : AuthenticationHandler options, + ILoggerFactory logger, + UrlEncoder encoder) : base(options, logger, encoder) + { + } +#else public DaprAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, @@ -32,6 +40,7 @@ public DaprAuthenticationHandler( ISystemClock clock) : base(options, logger, encoder, clock) { } +#endif protected override Task HandleAuthenticateAsync() { diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 45dd168fe..a3fd5b082 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,9 +1,5 @@  - - net6 - - diff --git a/src/Dapr.Client/DaprApiException.cs b/src/Dapr.Client/DaprApiException.cs index e7af8947c..75fc2cf7f 100644 --- a/src/Dapr.Client/DaprApiException.cs +++ b/src/Dapr.Client/DaprApiException.cs @@ -92,6 +92,9 @@ public DaprApiException(string message, Exception inner, string errorCode, bool /// /// The object that contains serialized object data of the exception being thrown. /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to the serialization ctor +#endif protected DaprApiException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -115,6 +118,9 @@ protected DaprApiException(SerializationInfo info, StreamingContext context) public bool IsTransient { get; } = false; /// +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to GetObjectData +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Client/DaprException.cs index 8c94a452d..e7b1efaba 100644 --- a/src/Dapr.Client/DaprException.cs +++ b/src/Dapr.Client/DaprException.cs @@ -47,6 +47,9 @@ public DaprException(string message, Exception innerException) /// /// The object that contains serialized object data of the exception being thrown. /// The object that contains contextual information about the source or destination. The context parameter is reserved for future use and can be null. +#if NET8_0_OR_GREATER + [Obsolete(DiagnosticId = "SYSLIB0051")] // add this attribute to GetObjectData +#endif protected DaprException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 0f1f64048..71fd0153e 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -1,7 +1,6 @@ - net6 enable diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index ef9722d36..d5820deb1 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -2,7 +2,8 @@ - net6;net7 + + net6;net7;net8 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cdfef31a6..2794f1b1f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,6 +3,7 @@ + net6;net8 $(RepoRoot)bin\$(Configuration)\prod\$(MSBuildProjectName)\ $(OutputPath)$(MSBuildProjectName).xml diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj index af0bfa13e..c06d651f4 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest.App/Dapr.Actors.AspNetCore.IntegrationTest.App.csproj @@ -1,9 +1,5 @@ - - net6;net7 - - diff --git a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj index 1b8590d7e..deccfc1e6 100644 --- a/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.Actors.AspNetCore.IntegrationTest/Dapr.Actors.AspNetCore.IntegrationTest.csproj @@ -1,8 +1,4 @@ - - net6;net7 - - 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 962963ac2..7e352d007 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -1,7 +1,6 @@ - net6;net7 Dapr.Actors.AspNetCore diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8a0fa16d7..8852dd465 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -1,6 +1,5 @@  - net6;net7 Dapr.Actors $(DefineConstants);ACTORS diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj index 0e15b6855..f415639be 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Dapr.AspNetCore.IntegrationTest.App.csproj @@ -1,9 +1,5 @@  - - net6;net7 - - diff --git a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj index c3cfd2cbf..3cd79d908 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj +++ b/test/Dapr.AspNetCore.IntegrationTest/Dapr.AspNetCore.IntegrationTest.csproj @@ -1,7 +1,4 @@  - - net6;net7 - diff --git a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj index 27f11a308..aa463be98 100644 --- a/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj +++ b/test/Dapr.AspNetCore.Test/Dapr.AspNetCore.Test.csproj @@ -1,7 +1,4 @@  - - net6;net7 - diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index c6de11e31..aef5b4113 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -1,7 +1,4 @@  - - net6;net7 - diff --git a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj index 0455d6db4..56ab3d222 100644 --- a/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj +++ b/test/Dapr.E2E.Test.Actors/Dapr.E2E.Test.Actors.csproj @@ -1,9 +1,5 @@ - - net6;net7 - - 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 8dcf009bc..849870b98 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,7 +1,4 @@ - - net6;net7 - diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj index dd087aef1..2fda3becc 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Dapr.E2E.Test.App.ReentrantActors.csproj @@ -1,9 +1,5 @@ - - net6;net7 - - 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 5c81557b8..e6ad11456 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -1,9 +1,5 @@ - - net6;net7 - - diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index 8b1448759..f899167c4 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -1,7 +1,4 @@  - - net6;net7 - diff --git a/test/Dapr.E2E.Test/DaprTestApp.cs b/test/Dapr.E2E.Test/DaprTestApp.cs index d65e21fd6..83f9948ac 100644 --- a/test/Dapr.E2E.Test/DaprTestApp.cs +++ b/test/Dapr.E2E.Test/DaprTestApp.cs @@ -132,16 +132,14 @@ public void Stop() private static string GetTargetFrameworkName() { var targetFrameworkName = ((TargetFrameworkAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(TargetFrameworkAttribute), false).FirstOrDefault()).FrameworkName; - string frameworkMoniker; - if (targetFrameworkName == ".NETCoreApp,Version=v6.0") - { - frameworkMoniker = "net6"; - } - else + + return targetFrameworkName switch { - frameworkMoniker = "net7"; - } - return frameworkMoniker; + ".NETCoreApp,Version=v6.0" => "net6", + ".NETCoreApp,Version=v7.0" => "net7", + ".NETCoreApp,Version=v8.0" => "net8", + _ => throw new InvalidOperationException($"Unsupported target framework: {targetFrameworkName}") + }; } private static (int, int, int, int) GetFreePorts() diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 0cd57ca67..2e4523582 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -1,7 +1,4 @@  - - net6;net7 - diff --git a/test/Directory.Build.props b/test/Directory.Build.props index e7d30ea2d..0ce23c19e 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,6 +2,8 @@ + net6;net7;net8 + $(RepoRoot)bin\$(Configuration)\test\$(MSBuildProjectName)\ From bb3f97abfd54f6f84fe35a83907754e77b5db1c0 Mon Sep 17 00:00:00 2001 From: MregXN <46479059+MregXN@users.noreply.github.com> Date: Wed, 29 Nov 2023 07:48:15 +0800 Subject: [PATCH 29/84] Modify broken links in README (#1190) Signed-off-by: MregXN --- examples/Workflow/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index e119465c3..b4f034376 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -11,7 +11,7 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and ## Optional Setup -Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/docs/api-token.md) +Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/blob/master/docs/api-tokens.md) ## Projects in sample From e435efda966f4d54ba3d75c8f0a4240d67057b47 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 28 Nov 2023 17:55:34 -0600 Subject: [PATCH 30/84] Added unit test to prove out enum serialization working as expected during event publish (#1174) Signed-off-by: Whit Waldo Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- test/Dapr.Client.Test/PublishEventApiTest.cs | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/Dapr.Client.Test/PublishEventApiTest.cs b/test/Dapr.Client.Test/PublishEventApiTest.cs index d8caf63d1..77d6ee905 100644 --- a/test/Dapr.Client.Test/PublishEventApiTest.cs +++ b/test/Dapr.Client.Test/PublishEventApiTest.cs @@ -11,6 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Immutable; +using System.Linq; +using System.Net.Http; +using System.Text.Json.Serialization; +using Grpc.Net.Client; + namespace Dapr.Client.Test { using System; @@ -51,6 +57,44 @@ public async Task PublishEventAsync_CanPublishTopicWithData() envelope.Metadata.Count.Should().Be(0); } + [Fact] + public async Task PublishEvent_ShouldRespectJsonStringEnumConverter() + { + //The following mimics how the TestClient is built, but adds the JsonStringEnumConverter to the serialization options + var handler = new TestClient.CapturingHandler(); + var httpClient = new HttpClient(handler); + var clientBuilder = new DaprClientBuilder() + .UseJsonSerializationOptions(new JsonSerializerOptions() + { + Converters = {new JsonStringEnumConverter(null, false)} + }) + .UseHttpClientFactory(() => httpClient) + .UseGrpcChannelOptions(new GrpcChannelOptions() + { + HttpClient = httpClient, ThrowOperationCanceledOnCancellation = true + }); + var client = new TestClient(clientBuilder.Build(), handler); + + //Ensure that the JsonStringEnumConverter is registered + client.InnerClient.JsonSerializerOptions.Converters.Count.Should().Be(1); + client.InnerClient.JsonSerializerOptions.Converters.First().GetType().Name.Should() + .Match(nameof(JsonStringEnumConverter)); + + var publishData = new Widget {Size = "Large", Color = WidgetColor.Red}; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + await daprClient.PublishEventAsync(TestPubsubName, "test", publishData); + }); + + request.Dismiss(); + + var envelope = await request.GetRequestEnvelopeAsync(); + var jsonFromRequest = envelope.Data.ToStringUtf8(); + jsonFromRequest.Should() + .Be(JsonSerializer.Serialize(publishData, client.InnerClient.JsonSerializerOptions)); + jsonFromRequest.Should().Match("{\"Size\":\"Large\",\"Color\":\"Red\"}"); + } + [Fact] public async Task PublishEventAsync_CanPublishTopicWithData_WithMetadata() { @@ -259,5 +303,18 @@ private class PublishData { public string PublishObjectParameter { get; set; } } + + private class Widget + { + public string Size { get; set; } + public WidgetColor Color { get; set; } + } + + private enum WidgetColor + { + Red, + Green, + Yellow + } } } From abcbf4f9a0cf6687229020c69b925d417740c443 Mon Sep 17 00:00:00 2001 From: MregXN <46479059+MregXN@users.noreply.github.com> Date: Wed, 29 Nov 2023 08:05:39 +0800 Subject: [PATCH 31/84] modify readme (#1192) Signed-off-by: MregXN Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- examples/Workflow/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index b4f034376..94473e6df 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -16,9 +16,17 @@ Dapr workflow, as well as this example program, now support authentication throu ## Projects in sample This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. -It utilizes the workflow SDK as well as the workflow management API for starting and querying workflows instances. -The main `Program.cs` file contains the main setup of the app, including the registration of the workflow and workflow activities. -The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. +It utilizes the workflow SDK as well as the workflow management API for simulating inventory management and sale of goods in a store. +The main `Program.cs` file contains the main setup of the app, the registration of the workflow and its activities, and interaction with the user. The workflow definition is found in the `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. + +There are five activities in the directory that could be called by the workflows: +- `NotifyActivity`: printing logs as notifications +- `ProcessPaymentActivity`: printing logs and delaying for simulating payment processing +- `RequestApprovalActivity`: printing logs to indicate that the order has been approved +- `ReserveInventoryActivity`: checking if there are enough items for purchase +- `UpdateInventoryActivity`: updating the statestore according to purchasing + +The `OrderProcessingWorkflow.cs` in `Workflows` directory implements the running logic of the workflow. Based on the purchase stage and outcome, it calls different activities and waits for the corresponding events to trigger interaction with the user. This sample also contains a [WorkflowUnitTest](./WorkflowUnitTest) .NET project that utilizes [xUnit](https://xunit.net/) and [Moq](https://github.com/moq/moq) to test the workflow logic. It works by creating an instance of the `OrderProcessingWorkflow` (defined in the `WorkflowConsoleApp` project), mocking activity calls, and testing the inputs and outputs. From b669585b22b83d045c9a87ec04ef9b31caa46743 Mon Sep 17 00:00:00 2001 From: Josh van Leeuwen Date: Wed, 29 Nov 2023 01:28:52 +0100 Subject: [PATCH 32/84] Updates Dapr to 1.12 in GitHub actions itest (#1185) * Updates Dapr to 1.12 in GitHub actions Signed-off-by: joshvanl * Remove commit ref from github actions Signed-off-by: joshvanl * Fix case sensitive error string match case Signed-off-by: joshvanl --------- Signed-off-by: joshvanl Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .github/workflows/itests.yml | 7 +++---- test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index e4c061428..8a299bd59 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -42,11 +42,10 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.9.1 - DAPR_RUNTIME_VER: 1.10.5 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/3dacfb672d55f1436c249057aaebbe597e1066f3/install/install.sh + DAPR_CLI_VER: 1.12.0 + DAPR_RUNTIME_VER: 1.12.0 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.12/install/install.sh DAPR_CLI_REF: '' - DAPR_REF: '4181de0edc65fc98a836ae7abc6042c575c8fae5' steps: - name: Set up Dapr CLI run: wget -q ${{ env.DAPR_INSTALL_URL }} -O - | /bin/bash -s ${{ env.DAPR_CLI_VER }} diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs index 979c136da..d95929ca3 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -138,7 +138,7 @@ public async Task TestWorkflows() } catch (DaprException ex) { - ex.InnerException.Message.Should().Contain("No such instance exists", $"Instance {instanceId} was not correctly purged"); + ex.InnerException.Message.Should().Contain("no such instance exists", $"Instance {instanceId} was not correctly purged"); } // Start another workflow for event raising purposes From 1cb00523a9676a12dce82b111d807ba1e438f72e Mon Sep 17 00:00:00 2001 From: Marc Duiker Date: Wed, 29 Nov 2023 01:38:09 +0100 Subject: [PATCH 33/84] Add holopin.yml config (#1147) Signed-off-by: Marc Duiker --- .github/holopin.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/holopin.yml diff --git a/.github/holopin.yml b/.github/holopin.yml new file mode 100644 index 000000000..44a7f0c8a --- /dev/null +++ b/.github/holopin.yml @@ -0,0 +1,6 @@ +organization: dapr +defaultSticker: clmjkxscc122740fl0mkmb7egi +stickers: + - + id: clmjkxscc122740fl0mkmb7egi + alias: ghc2023 From e8204dca455e984eb8ca9fd1f889e8353402dbdc Mon Sep 17 00:00:00 2001 From: Frank Buckley Date: Wed, 29 Nov 2023 19:09:13 +0000 Subject: [PATCH 34/84] Correct spelling of "identified" (#1159) Signed-off-by: Frank Buckley Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- src/Dapr.Client/DaprClient.cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 2a7299c78..361ac54bc 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -292,7 +292,7 @@ public abstract Task InvokeBindingAsync( /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the POST HTTP method. /// /// The Dapr application id to invoke the method on. @@ -305,7 +305,7 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the HTTP method specified by . /// /// The to use for the invocation request. @@ -316,7 +316,7 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the POST HTTP method and a JSON serialized request body specified by . /// /// The type of the data that will be JSON serialized and provided as the request body. @@ -331,7 +331,7 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// /// Creates an that can be used to perform service invocation for the - /// application idenfied by and invokes the method specified by + /// application identified by and invokes the method specified by /// with the HTTP method specified by and a JSON serialized request body specified by /// . /// @@ -444,7 +444,7 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri public abstract Task InvokeMethodAsync(HttpRequestMessage request, CancellationToken cancellationToken = default); /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the POST HTTP method and an empty request body. /// If the response has a non-success status code an exception will be thrown. /// @@ -462,7 +462,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the HTTP method specified by /// and an empty request body. If the response has a non-success status code an exception will be thrown. /// @@ -482,7 +482,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the POST HTTP method /// and a JSON serialized request body specified by . If the response has a non-success /// status code an exception will be thrown. @@ -504,7 +504,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the HTTP method specified by /// and a JSON serialized request body specified by . If the response has a non-success /// status code an exception will be thrown. @@ -528,7 +528,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the POST HTTP method /// and an empty request body. If the response has a success /// status code the body will be deserialized using JSON to a value of type ; @@ -549,7 +549,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the HTTP method specified by /// and an empty request body. If the response has a success /// status code the body will be deserialized using JSON to a value of type ; @@ -572,7 +572,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the POST HTTP method /// and a JSON serialized request body specified by . If the response has a success /// status code the body will be deserialized using JSON to a value of type ; @@ -596,7 +596,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation for the application idenfied by and invokes the method + /// Perform service invocation for the application identified by and invokes the method /// specified by with the HTTP method specified by /// and a JSON serialized request body specified by . If the response has a success /// status code the body will be deserialized using JSON to a value of type ; @@ -622,7 +622,7 @@ public Task InvokeMethodAsync( } /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method /// specified by with an empty request body. /// If the response has a non-success status code an exception will be thrown. /// @@ -636,7 +636,7 @@ public abstract Task InvokeMethodGrpcAsync( CancellationToken cancellationToken = default); /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method /// specified by with a Protobuf serialized request body specified by . /// If the response has a non-success status code an exception will be thrown. /// @@ -654,7 +654,7 @@ public abstract Task InvokeMethodGrpcAsync( where TRequest : IMessage; /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method /// specified by with an empty request body. If the response has a success /// status code the body will be deserialized using Protobuf to a value of type ; /// otherwise an exception will be thrown. @@ -671,7 +671,7 @@ public abstract Task InvokeMethodGrpcAsync( where TResponse : IMessage, new(); /// - /// Perform service invocation using gRPC semantics for the application idenfied by and invokes the method + /// Perform service invocation using gRPC semantics for the application identified by and invokes the method /// specified by with a Protobuf serialized request body specified by . If the response has a success /// status code the body will be deserialized using Protobuf to a value of type ; /// otherwise an exception will be thrown. From 10ef81873b3448fb136c73ad26a9fd2768954c2f Mon Sep 17 00:00:00 2001 From: Ryan Lettieri <67934986+RyanLettieri@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:39:56 -0700 Subject: [PATCH 35/84] Adding cancel to workflow example and updating api references to beta (#1194) Signed-off-by: Ryan Lettieri --- .../dotnet-workflow/dotnet-workflow-howto.md | 6 +++--- examples/Workflow/README.md | 8 ++++---- examples/Workflow/WorkflowConsoleApp/Program.cs | 9 ++++++++- examples/Workflow/WorkflowConsoleApp/demo.http | 8 ++++---- 4 files changed, 19 insertions(+), 12 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 650cdec38..f6d18bc58 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflow-howto.md @@ -83,7 +83,7 @@ Run the following command to start a workflow. {{% codetab %}} ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -93,7 +93,7 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing {{% codetab %}} ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=12345678 ` -H "Content-Type: application/json" ` -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -111,7 +111,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-alpha1/workflows/dapr/12345678 +curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/12345678 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed when you issue the HTTP request, you'll see the following JSON response (formatted for readability) with workflow status as `RUNNING`: diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 94473e6df..1af855767 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -61,7 +61,7 @@ For the workflow API option, two identical `curl` commands are shown, one for Li Make note of the "1234" in the commands below. This represents the unique identifier for the workflow run and can be replaced with any identifier of your choosing. ```bash -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 \ +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 \ -H "Content-Type: application/json" \ -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -69,7 +69,7 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing On Windows (PowerShell): ```powershell -curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 ` +curl -i -X POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=1234 ` -H "Content-Type: application/json" ` -d '{"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}' ``` @@ -83,7 +83,7 @@ If successful, you should see a response like the following: Next, send an HTTP request to get the status of the workflow that was started: ```bash -curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234 +curl -i -X GET http://localhost:3500/v1.0-beta1/workflows/dapr/1234 ``` The workflow is designed to take several seconds to complete. If the workflow hasn't completed yet when you issue the previous command, you should see the following JSON response (formatted for readability): @@ -132,4 +132,4 @@ info: WorkflowConsoleApp.Activities.NotifyActivity[0] Order 1234 processed successfully! ``` -If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). +If you have Zipkin configured for Dapr locally on your machine, then you can view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). \ No newline at end of file diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 9aae2427e..055b1b4c1 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -81,7 +81,14 @@ // Start the input loop using (daprClient) { - while (true) + bool quit = false; + Console.CancelKeyPress += delegate + { + quit = true; + Console.WriteLine("Shutting down the example."); + }; + + while (!quit) { // Get the name of the item to order and make sure we have inventory string items = string.Join(", ", baseInventory.Select(i => i.Name)); diff --git a/examples/Workflow/WorkflowConsoleApp/demo.http b/examples/Workflow/WorkflowConsoleApp/demo.http index 48b849a88..669cefeb7 100644 --- a/examples/Workflow/WorkflowConsoleApp/demo.http +++ b/examples/Workflow/WorkflowConsoleApp/demo.http @@ -1,17 +1,17 @@ ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx +POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1} ### Start order processing workflow - replace xxx with any id you like -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx +POST http://localhost:3500/v1.0-beta1/workflows/dapr/OrderProcessingWorkflow/start?instanceID=xxx Content-Type: application/json {"Name": "Cars", "TotalCost": 10000, "Quantity": 30} ### Query dapr sidecar - replace xxx with id from the workflow you've created above -GET http://localhost:3500/v1.0-alpha1/workflows/dapr/xxx +GET http://localhost:3500/v1.0-beta1/workflows/dapr/xxx ### Terminate the workflow - replace xxx with id from the workflow you've created above -POST http://localhost:3500/v1.0-alpha1/workflows/dapr/xxx/terminate \ No newline at end of file +POST http://localhost:3500/v1.0-beta1/workflows/dapr/xxx/terminate \ No newline at end of file From 8d06a1f9846f270d8e257356ce37c7e72db1d058 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 5 Jan 2024 16:43:51 -0800 Subject: [PATCH 36/84] Enable `CancellationToken` for non-remoting actor implementations (#1202) * Sketch no arguments with cancellation. Signed-off-by: Phillip Hoff * Sketch the other argument permutations. Signed-off-by: Phillip Hoff * Refactor tests. Signed-off-by: Phillip Hoff * Push HTTP request cancellation token down into handlers. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff --- .../ActorsEndpointRouteBuilderExtensions.cs | 8 +- src/Dapr.Actors/Runtime/ActorManager.cs | 8 +- .../Runtime/ActorRuntimeTests.cs | 106 ++++++++++++++++++ 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs index 55d161d9a..574a172a8 100644 --- a/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.Actors.AspNetCore/ActorsEndpointRouteBuilderExtensions.cs @@ -103,7 +103,7 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR try { - var (header, body) = await runtime.DispatchWithRemotingAsync(actorTypeName, actorId, methodName, daprActorheader, context.Request.Body); + var (header, body) = await runtime.DispatchWithRemotingAsync(actorTypeName, actorId, methodName, daprActorheader, context.Request.Body, context.RequestAborted); // Item 1 is header , Item 2 is body if (header != string.Empty) @@ -112,14 +112,14 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR context.Response.Headers[Constants.ErrorResponseHeaderName] = header; // add error header } - await context.Response.Body.WriteAsync(body, 0, body.Length); // add response message body + await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); // add response message body } catch (Exception ex) { var (header, body) = CreateExceptionResponseMessage(ex); context.Response.Headers[Constants.ErrorResponseHeaderName] = header; - await context.Response.Body.WriteAsync(body, 0, body.Length); + await context.Response.Body.WriteAsync(body, 0, body.Length, context.RequestAborted); } finally { @@ -130,7 +130,7 @@ private static IEndpointConventionBuilder MapActorMethodEndpoint(this IEndpointR { try { - await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body); + await runtime.DispatchWithoutRemotingAsync(actorTypeName, actorId, methodName, context.Request.Body, context.Response.Body, context.RequestAborted); } finally { diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index b7ee3bf3e..d766cd485 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -148,16 +148,16 @@ async Task RequestFunc(Actor actor, CancellationToken ct) var parameters = methodInfo.GetParameters(); dynamic awaitable; - if (parameters.Length == 0) + if (parameters.Length == 0 || (parameters.Length == 1 && parameters[0].ParameterType == typeof(CancellationToken))) { - awaitable = methodInfo.Invoke(actor, null); + awaitable = methodInfo.Invoke(actor, parameters.Length == 0 ? null : new object[] { ct }); } - else if (parameters.Length == 1) + else if (parameters.Length == 1 || (parameters.Length == 2 && parameters[1].ParameterType == typeof(CancellationToken))) { // deserialize using stream. var type = parameters[0].ParameterType; var deserializedType = await JsonSerializer.DeserializeAsync(requestBodyStream, type, jsonSerializerOptions); - awaitable = methodInfo.Invoke(actor, new object[] { deserializedType }); + awaitable = methodInfo.Invoke(actor, parameters.Length == 1 ? new object[] { deserializedType } : new object[] { deserializedType, ct }); } else { diff --git a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs index 52ae4aa7b..c74d0b754 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorRuntimeTests.cs @@ -27,6 +27,7 @@ namespace Dapr.Actors.Test using Xunit; using Dapr.Actors.Client; using System.Reflection; + using System.Threading; public sealed class ActorRuntimeTests { @@ -109,6 +110,111 @@ public async Task NoActivateMessageFromRuntime() Assert.Contains(actorType.Name, runtime.RegisteredActors.Select(a => a.Type.ActorTypeName), StringComparer.InvariantCulture); } + public interface INotRemotedActor : IActor + { + Task NoArgumentsAsync(); + + Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default); + + Task SingleArgumentAsync(bool arg); + + Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default); + } + + public sealed class NotRemotedActor : Actor, INotRemotedActor + { + public NotRemotedActor(ActorHost host) + : base(host) + { + } + + public Task NoArgumentsAsync() + { + return Task.FromResult(nameof(NoArgumentsAsync)); + } + + public Task NoArgumentsWithCancellationAsync(CancellationToken cancellationToken = default) + { + return Task.FromResult(nameof(NoArgumentsWithCancellationAsync)); + } + + public Task SingleArgumentAsync(bool arg) + { + return Task.FromResult(nameof(SingleArgumentAsync)); + } + + public Task SingleArgumentWithCancellationAsync(bool arg, CancellationToken cancellationToken = default) + { + return Task.FromResult(nameof(SingleArgumentWithCancellationAsync)); + } + } + + public async Task InvokeMethod(string methodName, object arg = null) where T : Actor + { + var options = new ActorRuntimeOptions(); + + options.Actors.RegisterActor(); + + var runtime = new ActorRuntime(options, loggerFactory, activatorFactory, proxyFactory); + + using var input = new MemoryStream(); + + if (arg is not null) + { + JsonSerializer.Serialize(input, arg); + + input.Seek(0, SeekOrigin.Begin); + } + + using var output = new MemoryStream(); + + await runtime.DispatchWithoutRemotingAsync(typeof(T).Name, ActorId.CreateRandom().ToString(), methodName, input, output); + + output.Seek(0, SeekOrigin.Begin); + + return JsonSerializer.Deserialize(output); + } + + [Fact] + public async Task NoRemotingMethodWithNoArguments() + { + string methodName = nameof(INotRemotedActor.NoArgumentsAsync); + + string result = await InvokeMethod(methodName); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithNoArgumentsWithCancellation() + { + string methodName = nameof(INotRemotedActor.NoArgumentsWithCancellationAsync); + + string result = await InvokeMethod(methodName); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithSingleArgument() + { + string methodName = nameof(INotRemotedActor.SingleArgumentAsync); + + string result = await InvokeMethod(methodName, true); + + Assert.Equal(methodName, result); + } + + [Fact] + public async Task NoRemotingMethodWithSingleArgumentWithCancellation() + { + string methodName = nameof(INotRemotedActor.SingleArgumentWithCancellationAsync); + + string result = await InvokeMethod(methodName, true); + + Assert.Equal(methodName, result); + } + [Fact] public async Task Actor_UsesCustomActivator() { From 0511b733a388ed6cab557297711f2db0d84b0bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Karstr=C3=B6m?= Date: Sat, 6 Jan 2024 01:59:48 +0100 Subject: [PATCH 37/84] Fix example dotnet-actors-howto.md (#1218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix example output to not repeat "Success" Signed-off-by: Henrik Karström Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- .../en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index ab41c3917..fdff16ffc 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 @@ -394,7 +394,7 @@ namespace MyActorClient Console.WriteLine($"Calling GetDataAsync on {actorType}:{actorId}..."); var savedData = await proxy.GetDataAsync(); - Console.WriteLine($"Got response: {response}"); + Console.WriteLine($"Got response: {savedData}"); } } } @@ -458,7 +458,7 @@ The projects that you've created can now to test the sample. Calling SetDataAsync on MyActor:1... Got response: Success Calling GetDataAsync on MyActor:1... - Got response: Success + Got response: PropertyA: ValueA, PropertyB: ValueB ``` > 💡 This sample relies on a few assumptions. The default listening port for an ASP.NET Core web project is 5000, which is being passed to `dapr run` as `--app-port 5000`. The default HTTP port for the Dapr sidecar is 3500. We're telling the sidecar for `MyActorService` to use 3500 so that `MyActorClient` can rely on the default value. From 72284066f78693ed1ac601f1ad8dce3571720ed9 Mon Sep 17 00:00:00 2001 From: Farshad Davoudi Date: Sat, 6 Jan 2024 04:39:27 +0330 Subject: [PATCH 38/84] Update _index.md by fixing broken link (#1221) Signed-off-by: Farshad Davoudi Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> --- daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index f68028dc5..41e610125 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -105,7 +105,7 @@ Console.WriteLine("Published deposit event!"); ``` - For a full list of state operations visit [How-To: Publish & subscribe]({{< ref howto-publish-subscribe.md >}}). -- Visit [.NET SDK examples](https://github.com/dapr/dotnet-sdk/tree/master/examples/client/PublishSubscribe) for code samples and instructions to try out pub/sub +- Visit [.NET SDK examples](https://github.com/dapr/dotnet-sdk/tree/master/examples/Client/PublishSubscribe) for code samples and instructions to try out pub/sub ### Interact with output bindings From 7616bfad2243ad078d0eb4d30dbdd957b0f5a291 Mon Sep 17 00:00:00 2001 From: MregXN <46479059+MregXN@users.noreply.github.com> Date: Tue, 9 Jan 2024 06:14:07 +0800 Subject: [PATCH 39/84] use daprWorkflowClient (#1212) Signed-off-by: MregXN --- .../Workflow/WorkflowConsoleApp/Program.cs | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 055b1b4c1..2b8213887 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -4,9 +4,9 @@ using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; const string StoreName = "statestore"; -const string DaprWorkflowComponent = "dapr"; // The workflow host is a background service that connects to the sidecar over gRPC var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => @@ -124,6 +124,8 @@ amount = 1; } + var daprWorkflowClient = host.Services.GetRequiredService(); + // Construct the order with a unique order ID string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; double totalCost = amount * item.PerItemCost; @@ -131,18 +133,16 @@ // Start the workflow using the order ID as the workflow ID Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); - await daprClient.StartWorkflowAsync( - workflowComponent: DaprWorkflowComponent, - workflowName: nameof(OrderProcessingWorkflow), + await daprWorkflowClient.ScheduleNewWorkflowAsync( + name: nameof(OrderProcessingWorkflow), input: orderInfo, instanceId: orderId); // Wait for the workflow to start and confirm the input - GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); + WorkflowState state = await daprWorkflowClient.WaitForWorkflowStartAsync( + instanceId: orderId); - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); + Console.WriteLine($"{nameof(OrderProcessingWorkflow)} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); // Wait for the workflow to complete while (true) @@ -150,22 +150,20 @@ await daprClient.StartWorkflowAsync( using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { - state = await daprClient.WaitForWorkflowCompletionAsync( + state = await daprWorkflowClient.WaitForWorkflowCompletionAsync( instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - cancellationToken: cts.Token); + cancellation: cts.Token); break; } catch (OperationCanceledException) { // Check to see if the workflow is blocked waiting for an approval - state = await daprClient.GetWorkflowAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); - if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && - customStatus.Contains("Waiting for approval")) + state = await daprWorkflowClient.GetWorkflowStateAsync( + instanceId: orderId); + + if(state.ReadCustomStatusAs()?.Contains("Waiting for approval") == true) { - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); + Console.WriteLine($"{nameof(OrderProcessingWorkflow)} (ID = {orderId}) requires approval. Approve? [Y/N]"); string approval = Console.ReadLine(); ApprovalResult approvalResult = ApprovalResult.Unspecified; if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) @@ -182,11 +180,10 @@ await daprClient.StartWorkflowAsync( if (approvalResult != ApprovalResult.Unspecified) { // Raise the workflow event to the workflow - await daprClient.RaiseWorkflowEventAsync( + await daprWorkflowClient.RaiseEventAsync( instanceId: orderId, - workflowComponent: DaprWorkflowComponent, eventName: "ManagerApproval", - eventData: approvalResult); + eventPayload: approvalResult); } // otherwise, keep waiting From 233c620b0fa8b331a464cc2dd126d0873fc35f3d Mon Sep 17 00:00:00 2001 From: Josh van Leeuwen Date: Mon, 8 Jan 2024 23:06:20 +0000 Subject: [PATCH 40/84] Actor State TTL (#1164) * Actor state TTL support Signed-off-by: joshvanl --- examples/Actor/ActorClient/Program.cs | 4 +- examples/Actor/DemoActor/DemoActor.cs | 10 +- examples/Actor/IDemoActor/IDemoActor.cs | 5 +- .../Communication/ActorStateResponse.cs | 50 +++++ src/Dapr.Actors/Constants.cs | 3 +- src/Dapr.Actors/DaprHttpInteractor.cs | 15 +- src/Dapr.Actors/IDaprInteractor.cs | 4 +- src/Dapr.Actors/Runtime/ActorStateChange.cs | 17 +- src/Dapr.Actors/Runtime/ActorStateManager.cs | 193 +++++++++++++++-- src/Dapr.Actors/Runtime/ConditionalValue.cs | 4 +- src/Dapr.Actors/Runtime/DaprStateProvider.cs | 30 ++- src/Dapr.Actors/Runtime/IActorStateManager.cs | 114 +++++++++- .../Runtime/IActorStateSerializer.cs | 2 +- .../Protos/dapr/proto/common/v1/common.proto | 2 +- .../Protos/dapr/proto/dapr/v1/dapr.proto | 92 ++++++-- .../Dapr.Actors.Test/ActorCodeBuilderTests.cs | 2 +- .../Dapr.Actors.Test/ActorStateManagerTest.cs | 199 ++++++++++++++++++ .../DaprHttpInteractorTest.cs | 57 ++++- .../Dapr.Actors.Test/DaprStateProviderTest.cs | 137 ++++++++++++ test/Dapr.Actors.Test/Runtime/ActorTests.cs | 2 +- test/Dapr.Actors.Test/TestDaprInteractor.cs | 8 +- test/Dapr.Client.Test/StateApiTest.cs | 2 +- .../Reminders/IReminderActor.cs | 2 +- .../Dapr.E2E.Test.Actors/State/IStateActor.cs | 26 +++ .../Actors/ReentrantActor.cs | 4 +- .../Dapr.E2E.Test.App/Actors/ReminderActor.cs | 2 +- test/Dapr.E2E.Test.App/Actors/StateActor.cs | 47 +++++ test/Dapr.E2E.Test.App/Actors/TimerActor.cs | 2 +- test/Dapr.E2E.Test.App/Startup.cs | 2 + .../Actors/E2ETests.StateTests.cs | 123 +++++++++++ .../configuration/featureconfig.yaml | 2 + 31 files changed, 1082 insertions(+), 80 deletions(-) create mode 100644 src/Dapr.Actors/Communication/ActorStateResponse.cs create mode 100644 test/Dapr.Actors.Test/ActorStateManagerTest.cs create mode 100644 test/Dapr.Actors.Test/DaprStateProviderTest.cs create mode 100644 test/Dapr.E2E.Test.Actors/State/IStateActor.cs create mode 100644 test/Dapr.E2E.Test.App/Actors/StateActor.cs create mode 100644 test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs diff --git a/examples/Actor/ActorClient/Program.cs b/examples/Actor/ActorClient/Program.cs index 5d7f06fb2..bae5d2ec2 100644 --- a/examples/Actor/ActorClient/Program.cs +++ b/examples/Actor/ActorClient/Program.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public static async Task Main(string[] args) var proxy = ActorProxy.Create(actorId, "DemoActor"); Console.WriteLine("Making call using actor proxy to save data."); - await proxy.SaveData(data); + await proxy.SaveData(data, TimeSpan.FromMinutes(10)); Console.WriteLine("Making call using actor proxy to get data."); var receivedData = await proxy.GetData(); Console.WriteLine($"Received data is {receivedData}."); diff --git a/examples/Actor/DemoActor/DemoActor.cs b/examples/Actor/DemoActor/DemoActor.cs index 62c100f79..da780d517 100644 --- a/examples/Actor/DemoActor/DemoActor.cs +++ b/examples/Actor/DemoActor/DemoActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -41,12 +41,12 @@ public DemoActor(ActorHost host, BankService bank) this.bank = bank; } - public async Task SaveData(MyData data) + public async Task SaveData(MyData data, TimeSpan ttl) { Console.WriteLine($"This is Actor id {this.Id} with data {data}."); // Set State using StateManager, state is saved after the method execution. - await this.StateManager.SetStateAsync(StateName, data); + await this.StateManager.SetStateAsync(StateName, data, ttl); } public Task GetData() @@ -109,7 +109,7 @@ public async Task ReceiveReminderAsync(string reminderName, byte[] state, TimeSp // This method is invoked when an actor reminder is fired. var actorState = await this.StateManager.GetStateAsync(StateName); actorState.PropertyB = $"Reminder triggered at '{DateTime.Now:yyyy-MM-ddTHH:mm:ss}'"; - await this.StateManager.SetStateAsync(StateName, actorState); + await this.StateManager.SetStateAsync(StateName, actorState, ttl: TimeSpan.FromMinutes(5)); } class TimerParams @@ -173,7 +173,7 @@ public async Task TimerCallback(byte[] data) { var state = await this.StateManager.GetStateAsync(StateName); state.PropertyA = $"Timer triggered at '{DateTime.Now:yyyyy-MM-ddTHH:mm:s}'"; - await this.StateManager.SetStateAsync(StateName, state); + await this.StateManager.SetStateAsync(StateName, state, ttl: TimeSpan.FromMinutes(5)); var timerParams = JsonSerializer.Deserialize(data); Console.WriteLine("Timer parameter1: " + timerParams.IntParam); Console.WriteLine("Timer parameter2: " + timerParams.StringParam); diff --git a/examples/Actor/IDemoActor/IDemoActor.cs b/examples/Actor/IDemoActor/IDemoActor.cs index c2926a048..25ce09370 100644 --- a/examples/Actor/IDemoActor/IDemoActor.cs +++ b/examples/Actor/IDemoActor/IDemoActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,8 +27,9 @@ public interface IDemoActor : IActor /// Method to save data. /// /// DAta to save. + /// TTL of state key. /// A task that represents the asynchronous save operation. - Task SaveData(MyData data); + Task SaveData(MyData data, TimeSpan ttl); /// /// Method to get data. diff --git a/src/Dapr.Actors/Communication/ActorStateResponse.cs b/src/Dapr.Actors/Communication/ActorStateResponse.cs new file mode 100644 index 000000000..22b3bf20e --- /dev/null +++ b/src/Dapr.Actors/Communication/ActorStateResponse.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Communication +{ + using System; + + /// + /// Represents a response from fetching an actor state key. + /// + public class ActorStateResponse + { + /// + /// Initializes a new instance of the class. + /// + /// The response value. + /// The time to live expiration time. + public ActorStateResponse(T value, DateTimeOffset? ttlExpireTime) + { + this.Value = value; + this.TTLExpireTime = ttlExpireTime; + } + + /// + /// Gets the response value as a string. + /// + /// + /// The response value as a string. + /// + public T Value { get; } + + /// + /// Gets the time to live expiration time. + /// + /// + /// The time to live expiration time. + /// + public DateTimeOffset? TTLExpireTime { get; } + } +} diff --git a/src/Dapr.Actors/Constants.cs b/src/Dapr.Actors/Constants.cs index be2d8f49f..038caf101 100644 --- a/src/Dapr.Actors/Constants.cs +++ b/src/Dapr.Actors/Constants.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ internal static class Constants public const string RequestHeaderName = "X-DaprRequestHeader"; public const string ErrorResponseHeaderName = "X-DaprErrorResponseHeader"; public const string ReentrancyRequestHeaderName = "Dapr-Reentrancy-Id"; + public const string TTLResponseHeaderName = "Metadata.ttlExpireTime"; public const string Dapr = "dapr"; public const string Config = "config"; public const string State = "state"; diff --git a/src/Dapr.Actors/DaprHttpInteractor.cs b/src/Dapr.Actors/DaprHttpInteractor.cs index 4695375fb..2565bab62 100644 --- a/src/Dapr.Actors/DaprHttpInteractor.cs +++ b/src/Dapr.Actors/DaprHttpInteractor.cs @@ -57,7 +57,7 @@ public DaprHttpInteractor( this.httpClient.Timeout = requestTimeout ?? this.httpClient.Timeout; } - public async Task GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) + public async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) { var relativeUrl = string.Format(CultureInfo.InvariantCulture, Constants.ActorStateKeyRelativeUrlFormat, actorType, actorId, keyName); @@ -72,7 +72,18 @@ HttpRequestMessage RequestFunc() using var response = await this.SendAsync(RequestFunc, relativeUrl, cancellationToken); var stringResponse = await response.Content.ReadAsStringAsync(); - return stringResponse; + + DateTimeOffset? ttlExpireTime = null; + if (response.Headers.TryGetValues(Constants.TTLResponseHeaderName, out IEnumerable headerValues)) + { + var ttlExpireTimeString = headerValues.First(); + if (!string.IsNullOrEmpty(ttlExpireTimeString)) + { + ttlExpireTime = DateTime.Parse(ttlExpireTimeString, CultureInfo.InvariantCulture); + } + } + + return new ActorStateResponse(stringResponse, ttlExpireTime); } public Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default) diff --git a/src/Dapr.Actors/IDaprInteractor.cs b/src/Dapr.Actors/IDaprInteractor.cs index 8f30aa18f..5849328a8 100644 --- a/src/Dapr.Actors/IDaprInteractor.cs +++ b/src/Dapr.Actors/IDaprInteractor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ internal interface IDaprInteractor /// Name of key to get value for. /// Cancels the operation. /// A task that represents the asynchronous operation. - Task GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); + Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default); /// /// Invokes Actor method. diff --git a/src/Dapr.Actors/Runtime/ActorStateChange.cs b/src/Dapr.Actors/Runtime/ActorStateChange.cs index c09e48df6..34fa68fdf 100644 --- a/src/Dapr.Actors/Runtime/ActorStateChange.cs +++ b/src/Dapr.Actors/Runtime/ActorStateChange.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,7 +27,8 @@ public sealed class ActorStateChange /// The type of value associated with given actor state name. /// The value associated with given actor state name. /// The kind of state change for given actor state name. - public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind) + /// The time to live for the state. + public ActorStateChange(string stateName, Type type, object value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) { ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); @@ -35,6 +36,7 @@ public ActorStateChange(string stateName, Type type, object value, StateChangeKi this.Type = type; this.Value = value; this.ChangeKind = changeKind; + this.TTLExpireTime = ttlExpireTime; } /// @@ -68,5 +70,16 @@ public ActorStateChange(string stateName, Type type, object value, StateChangeKi /// The kind of state change for given actor state name. /// public StateChangeKind ChangeKind { get; } + + /// + /// Gets the time to live for the state. + /// + /// + /// The time to live for the state. + /// + /// + /// If null, the state will not expire. + /// + public DateTimeOffset? TTLExpireTime { get; } } } diff --git a/src/Dapr.Actors/Runtime/ActorStateManager.cs b/src/Dapr.Actors/Runtime/ActorStateManager.cs index 9c752f56b..111bb80f4 100644 --- a/src/Dapr.Actors/Runtime/ActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/ActorStateManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Actors.Resources; +using Dapr.Actors.Communication; namespace Dapr.Actors.Runtime { @@ -38,14 +39,24 @@ internal ActorStateManager(Actor actor) public async Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); - + if (!(await this.TryAddStateAsync(stateName, value, cancellationToken))) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); } } - public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken) + public async Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + if (!(await this.TryAddStateAsync(stateName, value, ttl, cancellationToken))) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ActorStateAlreadyExists, stateName)); + } + } + + public async Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); @@ -57,8 +68,8 @@ public async Task TryAddStateAsync(string stateName, T value, Cancellat { var stateMetadata = stateChangeTracker[stateName]; - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) + // Check if the property was marked as remove or is expired in the cache + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) { stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update); return true; @@ -76,6 +87,37 @@ public async Task TryAddStateAsync(string stateName, T value, Cancellat return true; } + public async Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache or has been expired. + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) + { + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl); + return true; + } + + return false; + } + + if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + return false; + } + + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); + return true; + } + public async Task GetStateAsync(string stateName, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); @@ -102,8 +144,8 @@ public async Task> TryGetStateAsync(string stateName, Can { var stateMetadata = stateChangeTracker[stateName]; - // Check if the property was marked as remove in the cache - if (stateMetadata.ChangeKind == StateChangeKind.Remove) + // Check if the property was marked as remove in the cache or is expired + if (stateMetadata.ChangeKind == StateChangeKind.Remove || (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow)) { return new ConditionalValue(false, default); } @@ -114,10 +156,11 @@ public async Task> TryGetStateAsync(string stateName, Can var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); if (conditionalResult.HasValue) { - stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value, StateChangeKind.None)); + stateChangeTracker.Add(stateName, StateMetadata.Create(conditionalResult.Value.Value, StateChangeKind.None, ttlExpireTime: conditionalResult.Value.TTLExpireTime)); + return new ConditionalValue(true, conditionalResult.Value.Value); } - return conditionalResult; + return new ConditionalValue(false, default); } public async Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken) @@ -132,6 +175,7 @@ public async Task SetStateAsync(string stateName, T value, CancellationToken { var stateMetadata = stateChangeTracker[stateName]; stateMetadata.Value = value; + stateMetadata.TTLExpireTime = null; if (stateMetadata.ChangeKind == StateChangeKind.None || stateMetadata.ChangeKind == StateChangeKind.Remove) @@ -149,6 +193,36 @@ public async Task SetStateAsync(string stateName, T value, CancellationToken } } + public async Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + stateMetadata.Value = value; + stateMetadata.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl); + + if (stateMetadata.ChangeKind == StateChangeKind.None || + stateMetadata.ChangeKind == StateChangeKind.Remove) + { + stateMetadata.ChangeKind = StateChangeKind.Update; + } + } + else if (await this.actor.Host.StateProvider.ContainsStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken)) + { + stateChangeTracker.Add(stateName, StateMetadata.Create(value, StateChangeKind.Update, ttl: ttl)); + } + else + { + stateChangeTracker[stateName] = StateMetadata.Create(value, StateChangeKind.Add, ttl: ttl); + } + } + public async Task RemoveStateAsync(string stateName, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); @@ -171,6 +245,12 @@ public async Task TryRemoveStateAsync(string stateName, CancellationToken { var stateMetadata = stateChangeTracker[stateName]; + if (stateMetadata.TTLExpireTime.HasValue && stateMetadata.TTLExpireTime.Value <= DateTimeOffset.UtcNow) + { + stateChangeTracker.Remove(stateName); + return false; + } + switch (stateMetadata.ChangeKind) { case StateChangeKind.Remove: @@ -235,6 +315,24 @@ public async Task GetOrAddStateAsync(string stateName, T value, Cancellati return value; } + public async Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken) + { + EnsureStateProviderInitialized(); + + var condRes = await this.TryGetStateAsync(stateName, cancellationToken); + + if (condRes.HasValue) + { + return condRes.Value; + } + + var changeKind = this.IsStateMarkedForRemove(stateName) ? StateChangeKind.Update : StateChangeKind.Add; + + var stateChangeTracker = GetContextualStateTracker(); + stateChangeTracker[stateName] = StateMetadata.Create(value, changeKind, ttl: ttl); + return value; + } + public async Task AddOrUpdateStateAsync( string stateName, T addValue, @@ -272,7 +370,7 @@ public async Task AddOrUpdateStateAsync( var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); if (conditionalResult.HasValue) { - var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value); + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update)); return newValue; @@ -282,6 +380,54 @@ public async Task AddOrUpdateStateAsync( return addValue; } + public async Task AddOrUpdateStateAsync( + string stateName, + T addValue, + Func updateValueFactory, + TimeSpan ttl, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNull(stateName, nameof(stateName)); + + EnsureStateProviderInitialized(); + + var stateChangeTracker = GetContextualStateTracker(); + + if (stateChangeTracker.ContainsKey(stateName)) + { + var stateMetadata = stateChangeTracker[stateName]; + + // Check if the property was marked as remove in the cache + if (stateMetadata.ChangeKind == StateChangeKind.Remove) + { + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Update, ttl: ttl); + return addValue; + } + + var newValue = updateValueFactory.Invoke(stateName, (T)stateMetadata.Value); + stateMetadata.Value = newValue; + + if (stateMetadata.ChangeKind == StateChangeKind.None) + { + stateMetadata.ChangeKind = StateChangeKind.Update; + } + + return newValue; + } + + var conditionalResult = await this.TryGetStateFromStateProviderAsync(stateName, cancellationToken); + if (conditionalResult.HasValue) + { + var newValue = updateValueFactory.Invoke(stateName, conditionalResult.Value.Value); + stateChangeTracker.Add(stateName, StateMetadata.Create(newValue, StateChangeKind.Update, ttl: ttl)); + + return newValue; + } + + stateChangeTracker[stateName] = StateMetadata.Create(addValue, StateChangeKind.Add, ttl: ttl); + return addValue; + } + public Task ClearCacheAsync(CancellationToken cancellationToken) { EnsureStateProviderInitialized(); @@ -310,7 +456,7 @@ public async Task SaveStateAsync(CancellationToken cancellationToken = default) if (stateMetadata.ChangeKind != StateChangeKind.None) { stateChangeList.Add( - new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind)); + new ActorStateChange(stateName, stateMetadata.Type, stateMetadata.Value, stateMetadata.ChangeKind, stateMetadata.TTLExpireTime)); if (stateMetadata.ChangeKind == StateChangeKind.Remove) { @@ -362,7 +508,7 @@ private bool IsStateMarkedForRemove(string stateName) return false; } - private Task> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) + private Task>> TryGetStateFromStateProviderAsync(string stateName, CancellationToken cancellationToken) { EnsureStateProviderInitialized(); return this.actor.Host.StateProvider.TryLoadStateAsync(this.actorTypeName, this.actor.Id.ToString(), stateName, cancellationToken); @@ -392,11 +538,20 @@ private Dictionary GetContextualStateTracker() private sealed class StateMetadata { - private StateMetadata(object value, Type type, StateChangeKind changeKind) + private StateMetadata(object value, Type type, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime = null, TimeSpan? ttl = null) { this.Value = value; this.Type = type; this.ChangeKind = changeKind; + + if (ttlExpireTime.HasValue && ttl.HasValue) { + throw new ArgumentException("Cannot specify both TTLExpireTime and TTL"); + } + if (ttl.HasValue) { + this.TTLExpireTime = DateTimeOffset.UtcNow.Add(ttl.Value); + } else { + this.TTLExpireTime = ttlExpireTime; + } } public object Value { get; set; } @@ -405,11 +560,23 @@ private StateMetadata(object value, Type type, StateChangeKind changeKind) public Type Type { get; } + public DateTimeOffset? TTLExpireTime { get; set; } + public static StateMetadata Create(T value, StateChangeKind changeKind) { return new StateMetadata(value, typeof(T), changeKind); } + public static StateMetadata Create(T value, StateChangeKind changeKind, DateTimeOffset? ttlExpireTime) + { + return new StateMetadata(value, typeof(T), changeKind, ttlExpireTime: ttlExpireTime); + } + + public static StateMetadata Create(T value, StateChangeKind changeKind, TimeSpan? ttl) + { + return new StateMetadata(value, typeof(T), changeKind, ttl: ttl); + } + public static StateMetadata CreateForRemove() { return new StateMetadata(null, typeof(object), StateChangeKind.Remove); diff --git a/src/Dapr.Actors/Runtime/ConditionalValue.cs b/src/Dapr.Actors/Runtime/ConditionalValue.cs index 1d2a197eb..ec4f3a5a6 100644 --- a/src/Dapr.Actors/Runtime/ConditionalValue.cs +++ b/src/Dapr.Actors/Runtime/ConditionalValue.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,4 +42,4 @@ public ConditionalValue(bool hasValue, TValue value) /// The value of the object. If HasValue is false, returns the default value for type of the TValue parameter. public TValue Value { get; } } -} \ No newline at end of file +} diff --git a/src/Dapr.Actors/Runtime/DaprStateProvider.cs b/src/Dapr.Actors/Runtime/DaprStateProvider.cs index ae86fb28b..e81308dbd 100644 --- a/src/Dapr.Actors/Runtime/DaprStateProvider.cs +++ b/src/Dapr.Actors/Runtime/DaprStateProvider.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ namespace Dapr.Actors.Runtime using System.Text.Json; using System.Threading; using System.Threading.Tasks; + using Dapr.Actors.Communication; /// /// State Provider to interact with Dapr runtime. @@ -43,27 +44,27 @@ public DaprStateProvider(IDaprInteractor daprInteractor, JsonSerializerOptions j this.daprInteractor = daprInteractor; } - public async Task> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) + public async Task>> TryLoadStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) { - var result = new ConditionalValue(false, default); - var stringResult = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + var result = new ConditionalValue>(false, default); + var response = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - if (stringResult.Length != 0) + if (response.Value.Length != 0 && (!response.TTLExpireTime.HasValue || response.TTLExpireTime.Value > DateTimeOffset.UtcNow)) { T typedResult; // perform default json de-serialization if custom serializer was not provided. if (this.actorStateSerializer != null) { - var byteResult = Convert.FromBase64String(stringResult.Trim('"')); + var byteResult = Convert.FromBase64String(response.Value.Trim('"')); typedResult = this.actorStateSerializer.Deserialize(byteResult); } else { - typedResult = JsonSerializer.Deserialize(stringResult, jsonSerializerOptions); + typedResult = JsonSerializer.Deserialize(response.Value, jsonSerializerOptions); } - result = new ConditionalValue(true, typedResult); + result = new ConditionalValue>(true, new ActorStateResponse(typedResult, response.TTLExpireTime)); } return result; @@ -71,8 +72,8 @@ public async Task> TryLoadStateAsync(string actorType, st public async Task ContainsStateAsync(string actorType, string actorId, string stateName, CancellationToken cancellationToken = default) { - var byteResult = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); - return byteResult.Length != 0; + var result = await this.daprInteractor.GetStateAsync(actorType, actorId, stateName, cancellationToken); + return (result.Value.Length != 0 && (!result.TTLExpireTime.HasValue || result.TTLExpireTime.Value > DateTimeOffset.UtcNow)); } public async Task SaveStateAsync(string actorType, string actorId, IReadOnlyCollection stateChanges, CancellationToken cancellationToken = default) @@ -132,6 +133,15 @@ private async Task DoStateChangesTransactionallyAsync(string actorType, string a writer.WritePropertyName("value"); JsonSerializer.Serialize(writer, stateChange.Value, stateChange.Type, jsonSerializerOptions); } + + if (stateChange.TTLExpireTime.HasValue) { + var ttl = (int)Math.Ceiling((stateChange.TTLExpireTime.Value - DateTimeOffset.UtcNow).TotalSeconds); + writer.WritePropertyName("metadata"); + writer.WriteStartObject(); + writer.WriteString("ttlInSeconds", ttl.ToString()); + writer.WriteEndObject(); + } + break; default: break; diff --git a/src/Dapr.Actors/Runtime/IActorStateManager.cs b/src/Dapr.Actors/Runtime/IActorStateManager.cs index df1eb3356..b85fa2a06 100644 --- a/src/Dapr.Actors/Runtime/IActorStateManager.cs +++ b/src/Dapr.Actors/Runtime/IActorStateManager.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ namespace Dapr.Actors.Runtime using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Dapr.Actors.Communication; /// /// Represents an interface that exposes methods to manage state of an . @@ -45,6 +46,28 @@ public interface IActorStateManager /// Task AddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Adds an actor state with given state name. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The token to monitor for cancellation requests. + /// The time to live for the state. + /// + /// A task that represents the asynchronous add operation. + /// + /// + /// An actor state with given state name already exists. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// /// Gets an actor state with specified state name. /// @@ -85,6 +108,26 @@ public interface IActorStateManager /// Task SetStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Sets an actor state with given state name to specified value. + /// If an actor state with specified name does not exist, it is added. + /// + /// Type of value associated with given state name. + /// Name of the actor state to set. + /// Value of the actor state. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous set operation. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task SetStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// /// Removes an actor state with specified state name. /// @@ -121,6 +164,30 @@ public interface IActorStateManager /// Task TryAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Attempts to add an actor state with given state name and value. Returns false if an actor state with + /// the same name already exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add. + /// Value of the actor state to add. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// This is optional and defaults to . + /// + /// A boolean task that represents the asynchronous add operation. Returns true if the + /// value was successfully added and false if an actor state with the same name already exists. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task TryAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// /// Attempts to get an actor state with specified state name. /// @@ -188,6 +255,29 @@ public interface IActorStateManager /// Task GetOrAddStateAsync(string stateName, T value, CancellationToken cancellationToken = default); + /// + /// Gets an actor state with the given state name if it exists. If it does not + /// exist, creates and new state with the specified name and value. + /// + /// Type of value associated with given state name. + /// Name of the actor state to get or add. + /// Value of the actor state to add if it does not exist. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous get or add operation. The value of TResult + /// parameter contains value of actor state with given state name. + /// + /// The specified state name is null. + /// Provide a valid state name string. + /// The request was canceled using the specified + /// . + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task GetOrAddStateAsync(string stateName, T value, TimeSpan ttl, CancellationToken cancellationToken = default); + /// /// Adds an actor state with given state name, if it does not already exist or updates /// the state with specified state name, if it exists. @@ -209,6 +299,28 @@ public interface IActorStateManager /// Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, CancellationToken cancellationToken = default); + /// + /// Adds an actor state with given state name, if it does not already exist or updates + /// the state with specified state name, if it exists. + /// + /// Type of value associated with given state name. + /// Name of the actor state to add or update. + /// Value of the actor state to add if it does not exist. + /// Factory function to generate value of actor state to update if it exists. + /// The time to live for the state. + /// The token to monitor for cancellation requests. + /// + /// A task that represents the asynchronous add/update operation. The value of TResult + /// parameter contains value of actor state that was added/updated. + /// + /// The specified state name is null. + /// The operation was canceled. + /// + /// The type of state value must be + /// Data Contract serializable. + /// + Task AddOrUpdateStateAsync(string stateName, T addValue, Func updateValueFactory, TimeSpan ttl, CancellationToken cancellationToken = default); + /// /// Clears all the cached actor states and any operation(s) performed on /// since last state save operation. diff --git a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs index c6136c057..cff3b7c26 100644 --- a/src/Dapr.Actors/Runtime/IActorStateSerializer.cs +++ b/src/Dapr.Actors/Runtime/IActorStateSerializer.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto index 3faea3016..1e63b885d 100644 --- a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto @@ -77,7 +77,7 @@ message InvokeRequest { HTTPExtension http_extension = 4; } -// InvokeResponse is the response message inclduing data and its content type +// InvokeResponse is the response message including data and its content type // from app callback. // This message is used in InvokeService of Dapr gRPC Service and OnInvoke // of AppCallback gRPC service. diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto index 883527adb..eafb5452e 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto @@ -169,6 +169,26 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Starts a new instance of a workflow + rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowBeta1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowBeta1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowBeta1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowBeta1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowBeta1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} // Shutdown the sidecar rpc Shutdown (google.protobuf.Empty) returns (google.protobuf.Empty) {} } @@ -542,6 +562,9 @@ message GetActorStateRequest { // GetActorStateResponse is the response conveying the actor's state value. message GetActorStateResponse { bytes data = 1; + + // The metadata which will be sent to app. + map metadata = 2; } // ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. @@ -580,10 +603,14 @@ message InvokeActorResponse { // GetMetadataResponse is a message that is returned on GetMetadata rpc call message GetMetadataResponse { string id = 1; - repeated ActiveActorsCount active_actors_count = 2; - repeated RegisteredComponents registered_components = 3; - map extended_metadata = 4; - repeated PubsubSubscription subscriptions = 5; + repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors"]; + repeated RegisteredComponents registered_components = 3 [json_name = "components"]; + map extended_metadata = 4 [json_name = "extended"]; + repeated PubsubSubscription subscriptions = 5 [json_name = "subscriptions"]; + repeated MetadataHTTPEndpoint http_endpoints = 6 [json_name = "httpEndpoints"]; + AppConnectionProperties app_connection_properties = 7 [json_name = "appConnectionProperties"]; + string runtime_version = 8 [json_name = "runtimeVersion"]; + repeated string enabled_features = 9 [json_name = "enabledFeatures"]; } message ActiveActorsCount { @@ -598,12 +625,31 @@ message RegisteredComponents { repeated string capabilities = 4; } +message MetadataHTTPEndpoint { + string name = 1 [json_name = "name"]; +} + +message AppConnectionProperties { + int32 port = 1; + string protocol = 2; + string channel_address = 3 [json_name = "channelAddress"]; + int32 max_concurrency = 4 [json_name = "maxConcurrency"]; + AppConnectionHealthProperties health = 5; +} + +message AppConnectionHealthProperties { + string health_check_path = 1 [json_name = "healthCheckPath"]; + string health_probe_interval = 2 [json_name = "healthProbeInterval"]; + string health_probe_timeout = 3 [json_name = "healthProbeTimeout"]; + int32 health_threshold = 4 [json_name = "healthThreshold"]; +} + message PubsubSubscription { - string pubsub_name = 1; - string topic = 2; - map metadata = 3; - PubsubSubscriptionRules rules = 4; - string dead_letter_topic = 5; + string pubsub_name = 1 [json_name = "pubsubname"]; + string topic = 2 [json_name = "topic"]; + map metadata = 3 [json_name = "metadata"]; + PubsubSubscriptionRules rules = 4 [json_name = "rules"]; + string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; } message PubsubSubscriptionRules { @@ -900,7 +946,7 @@ message EncryptRequest { // Request details. Must be present in the first message only. EncryptRequestOptions options = 1; // Chunk of data of arbitrary size. - // common.v1.StreamPayload payload = 2; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 2; } // EncryptRequestOptions contains options for the first message in the EncryptAlpha1 request. @@ -928,7 +974,7 @@ message EncryptRequestOptions { // EncryptResponse is the response for EncryptAlpha1. message EncryptResponse { // Chunk of data. - // common.v1.StreamPayload payload = 1; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 1; } // DecryptRequest is the request for DecryptAlpha1. @@ -936,7 +982,7 @@ message DecryptRequest { // Request details. Must be present in the first message only. DecryptRequestOptions options = 1; // Chunk of data of arbitrary size. - // common.v1.StreamPayload payload = 2; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 2; } // DecryptRequestOptions contains options for the first message in the DecryptAlpha1 request. @@ -952,10 +998,10 @@ message DecryptRequestOptions { // DecryptResponse is the response for DecryptAlpha1. message DecryptResponse { // Chunk of data. - // common.v1.StreamPayload payload = 1; // TODO: Commented out since it was causing an issue + common.v1.StreamPayload payload = 1; } -// GetWorkflowRequest is the request for GetWorkflowAlpha1. +// GetWorkflowRequest is the request for GetWorkflowBeta1. message GetWorkflowRequest { // ID of the workflow instance to query. string instance_id = 1 [json_name = "instanceID"]; @@ -963,7 +1009,7 @@ message GetWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// GetWorkflowResponse is the response for GetWorkflowAlpha1. +// GetWorkflowResponse is the response for GetWorkflowBeta1. message GetWorkflowResponse { // ID of the workflow instance. string instance_id = 1 [json_name = "instanceID"]; @@ -979,7 +1025,7 @@ message GetWorkflowResponse { map properties = 6; } -// StartWorkflowRequest is the request for StartWorkflowAlpha1. +// StartWorkflowRequest is the request for StartWorkflowBeta1. message StartWorkflowRequest { // The ID to assign to the started workflow instance. If empty, a random ID is generated. string instance_id = 1 [json_name = "instanceID"]; @@ -993,13 +1039,13 @@ message StartWorkflowRequest { bytes input = 5; } -// StartWorkflowResponse is the response for StartWorkflowAlpha1. +// StartWorkflowResponse is the response for StartWorkflowBeta1. message StartWorkflowResponse { // ID of the started workflow instance. string instance_id = 1 [json_name = "instanceID"]; } -// TerminateWorkflowRequest is the request for TerminateWorkflowAlpha1. +// TerminateWorkflowRequest is the request for TerminateWorkflowBeta1. message TerminateWorkflowRequest { // ID of the workflow instance to terminate. string instance_id = 1 [json_name = "instanceID"]; @@ -1007,7 +1053,7 @@ message TerminateWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// PauseWorkflowRequest is the request for PauseWorkflowAlpha1. +// PauseWorkflowRequest is the request for PauseWorkflowBeta1. message PauseWorkflowRequest { // ID of the workflow instance to pause. string instance_id = 1 [json_name = "instanceID"]; @@ -1015,7 +1061,7 @@ message PauseWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// ResumeWorkflowRequest is the request for ResumeWorkflowAlpha1. +// ResumeWorkflowRequest is the request for ResumeWorkflowBeta1. message ResumeWorkflowRequest { // ID of the workflow instance to resume. string instance_id = 1 [json_name = "instanceID"]; @@ -1023,7 +1069,7 @@ message ResumeWorkflowRequest { string workflow_component = 2 [json_name = "workflowComponent"]; } -// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowAlpha1. +// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowBeta1. message RaiseEventWorkflowRequest { // ID of the workflow instance to raise an event for. string instance_id = 1 [json_name = "instanceID"]; @@ -1035,10 +1081,10 @@ message RaiseEventWorkflowRequest { bytes event_data = 4; } -// PurgeWorkflowRequest is the request for PurgeWorkflowAlpha1. +// PurgeWorkflowRequest is the request for PurgeWorkflowBeta1. message PurgeWorkflowRequest { // ID of the workflow instance to purge. string instance_id = 1 [json_name = "instanceID"]; // Name of the workflow component. string workflow_component = 2 [json_name = "workflowComponent"]; -} \ No newline at end of file +} diff --git a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs index 93f6ba92f..6bb3c827d 100644 --- a/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs +++ b/test/Dapr.Actors.Test/ActorCodeBuilderTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Actors.Test/ActorStateManagerTest.cs b/test/Dapr.Actors.Test/ActorStateManagerTest.cs new file mode 100644 index 000000000..a6517a6b4 --- /dev/null +++ b/test/Dapr.Actors.Test/ActorStateManagerTest.cs @@ -0,0 +1,199 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Test +{ + using System; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Security; + using System.Security.Authentication; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using System.Collections.Generic; + using FluentAssertions; + using Xunit; + using Dapr.Actors.Communication; + using Dapr.Actors.Runtime; + using Moq; + + /// + /// Contains tests for ActorStateManager. + /// + public class ActorStateManagerTest + { + [Fact] + public async Task SetGet() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key2", "value4", token)); + + await mngr.SetStateAsync("key1", "value5", token); + await mngr.SetStateAsync("key2", "value6", token); + Assert.Equal("value5", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value6", await mngr.GetStateAsync("key2", token)); + } + + [Fact] + public async Task StateWithTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await Task.Delay(TimeSpan.FromSeconds(1.5)); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + + // Should be able to add state again after expiry and should not expire. + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + await Task.Delay(TimeSpan.FromSeconds(1.5)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + } + + [Fact] + public async Task StateRemoveAddTTL() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await mngr.SetStateAsync("key1", "value1", token); + await mngr.SetStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + // TTL is removed so state should not expire. + await Task.Delay(TimeSpan.FromSeconds(1.5)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + // Adding TTL back should expire state. + await mngr.SetStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.SetStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + await Task.Delay(TimeSpan.FromSeconds(1.5)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + } + + [Fact] + public async Task StateDaprdExpireTime() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + // Existing key which has an expiry time. + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value1\"", DateTime.UtcNow.AddSeconds(1)))); + + await Assert.ThrowsAsync(() => mngr.AddStateAsync("key1", "value3", token)); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + + // No longer return the value from the state provider. + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + // Key should be expired after 1 seconds. + await Task.Delay(TimeSpan.FromSeconds(1.5)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); + await mngr.AddStateAsync("key1", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value2", await mngr.GetStateAsync("key1", token)); + } + + [Fact] + public async Task RemoveState() + { + var interactor = new Mock(); + var host = ActorHost.CreateForTest(); + host.StateProvider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var mngr = new ActorStateManager(new TestActor(host)); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + + await Assert.ThrowsAsync(() => mngr.RemoveStateAsync("key1", token)); + + await mngr.AddStateAsync("key1", "value1", token); + await mngr.AddStateAsync("key2", "value2", token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + + await mngr.RemoveStateAsync("key1", token); + await mngr.RemoveStateAsync("key2", token); + + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key1", token)); + await Assert.ThrowsAsync(() => mngr.GetStateAsync("key2", token)); + + // Should be able to add state again after removal. + await mngr.AddStateAsync("key1", "value1", TimeSpan.FromSeconds(1), token); + await mngr.AddStateAsync("key2", "value2", TimeSpan.FromSeconds(1), token); + Assert.Equal("value1", await mngr.GetStateAsync("key1", token)); + Assert.Equal("value2", await mngr.GetStateAsync("key2", token)); + } + } +} diff --git a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs index 80dae342f..21c142267 100644 --- a/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs +++ b/test/Dapr.Actors.Test/DaprHttpInteractorTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ namespace Dapr.Actors.Test { + using System; using System.Globalization; using System.Linq; using System.Net; @@ -23,6 +24,7 @@ namespace Dapr.Actors.Test using System.Threading.Tasks; using FluentAssertions; using Xunit; + using Dapr.Actors.Communication; /// /// Contains tests for DaprHttpInteractor. @@ -350,5 +352,58 @@ public async Task InvokeActorMethodOmitsReentrancyIdIfNotSet_ValidateHeaders() request.Dismiss(); Assert.False(request.Request.Headers.Contains(Constants.ReentrancyRequestHeaderName)); } + + [Fact] + public async Task GetState_TTLExpireTimeExists() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + var actorType = "ActorType_Test"; + var actorId = "ActorId_Test"; + var keyName = "StateKey_Test"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + return await httpInteractor.GetStateAsync(actorType, actorId, keyName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("test"), + Headers = + { + { "Metadata.ttlExpireTime", "2023-04-05T23:22:21Z" }, + }, + }; + + var actual = await request.CompleteAsync(message); + Assert.Equal("test", actual.Value); + var expTTL = new DateTimeOffset(2023, 04, 05, 23, 22, 21, 0, new GregorianCalendar(), new TimeSpan(0, 0, 0)); + Assert.Equal(expTTL, actual.TTLExpireTime); + } + + [Fact] + public async Task GetState_TTLExpireTimeNotExists() + { + await using var client = TestClient.CreateForDaprHttpInterator(); + + var actorType = "ActorType_Test"; + var actorId = "ActorId_Test"; + var keyName = "StateKey_Test"; + + var request = await client.CaptureHttpRequestAsync(async httpInteractor => + { + return await httpInteractor.GetStateAsync(actorType, actorId, keyName); + }); + + var message = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("test"), + }; + + var actual = await request.CompleteAsync(message); + Assert.Equal("test", actual.Value); + Assert.False(actual.TTLExpireTime.HasValue); + } } } diff --git a/test/Dapr.Actors.Test/DaprStateProviderTest.cs b/test/Dapr.Actors.Test/DaprStateProviderTest.cs new file mode 100644 index 000000000..63be89e95 --- /dev/null +++ b/test/Dapr.Actors.Test/DaprStateProviderTest.cs @@ -0,0 +1,137 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Test +{ + using System; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Security; + using System.Security.Authentication; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using System.Collections.Generic; + using FluentAssertions; + using Xunit; + using Dapr.Actors.Communication; + using Dapr.Actors.Runtime; + using Moq; + + /// + /// Contains tests for DaprStateProvider. + /// + public class DaprStateProviderTest + { + [Fact] + public async Task SaveStateAsync() + { + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); + + var stateChangeList = new List(); + stateChangeList.Add( + new ActorStateChange("key1", typeof(string), "value1", StateChangeKind.Add, DateTimeOffset.UtcNow.Add(TimeSpan.FromSeconds(2)))); + stateChangeList.Add( + new ActorStateChange("key2", typeof(string), "value2", StateChangeKind.Add, null)); + + string content = null; + interactor + .Setup(d => d.SaveStateTransactionallyAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback((actorType, actorId, data, token) => content = data) + .Returns(Task.FromResult(true)); + + await provider.SaveStateAsync("actorType", "actorId", stateChangeList, token); + Assert.Equal( + "[{\"operation\":\"upsert\",\"request\":{\"key\":\"key1\",\"value\":\"value1\",\"metadata\":{\"ttlInSeconds\":\"2\"}}},{\"operation\":\"upsert\",\"request\":{\"key\":\"key2\",\"value\":\"value2\"}}]", + content + ); + } + + [Fact] + public async Task ContainsStateAsync() + { + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); + Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + var ttl = DateTime.UtcNow.AddSeconds(1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + Assert.True(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + + ttl = DateTime.UtcNow.AddSeconds(-1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + Assert.False(await provider.ContainsStateAsync("actorType", "actorId", "key", token)); + } + + [Fact] + public async Task TryLoadStateAsync() + { + var interactor = new Mock(); + var provider = new DaprStateProvider(interactor.Object, new JsonSerializerOptions()); + var token = new CancellationToken(); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("", null))); + var resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.False(resp.HasValue); + + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", null))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.True(resp.HasValue); + Assert.Equal("value", resp.Value.Value); + Assert.False(resp.Value.TTLExpireTime.HasValue); + + var ttl = DateTime.UtcNow.AddSeconds(1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.True(resp.HasValue); + Assert.Equal("value", resp.Value.Value); + Assert.True(resp.Value.TTLExpireTime.HasValue); + Assert.Equal(ttl, resp.Value.TTLExpireTime.Value); + + ttl = DateTime.UtcNow.AddSeconds(-1); + interactor + .Setup(d => d.GetStateAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(new ActorStateResponse("\"value\"", ttl))); + resp = await provider.TryLoadStateAsync("actorType", "actorId", "key", token); + Assert.False(resp.HasValue); + } + } +} diff --git a/test/Dapr.Actors.Test/Runtime/ActorTests.cs b/test/Dapr.Actors.Test/Runtime/ActorTests.cs index b18800f0e..f88b4e03f 100644 --- a/test/Dapr.Actors.Test/Runtime/ActorTests.cs +++ b/test/Dapr.Actors.Test/Runtime/ActorTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Actors.Test/TestDaprInteractor.cs b/test/Dapr.Actors.Test/TestDaprInteractor.cs index 92cfa7096..11f88e684 100644 --- a/test/Dapr.Actors.Test/TestDaprInteractor.cs +++ b/test/Dapr.Actors.Test/TestDaprInteractor.cs @@ -67,10 +67,10 @@ public Task InvokeActorMethodWithoutRemotingAsync(string actorType, stri /// JSON data with state changes as per the Dapr spec for transaction state update. /// Cancels the operation. /// A task that represents the asynchronous operation. - public Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, + public virtual async Task SaveStateTransactionallyAsync(string actorType, string actorId, string data, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + await _testDaprInteractor.SaveStateTransactionallyAsync(actorType, actorId, data); } /// @@ -81,9 +81,9 @@ public Task SaveStateTransactionallyAsync(string actorType, string actorId, stri /// Name of key to get value for. /// Cancels the operation. /// A task that represents the asynchronous operation. - public Task GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) + public virtual async Task> GetStateAsync(string actorType, string actorId, string keyName, CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); + return await _testDaprInteractor.GetStateAsync(actorType, actorId, keyName); } /// diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index 90c06e6b1..cfa664663 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs index 0bf57f64c..c0e3f86a2 100644 --- a/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs +++ b/test/Dapr.E2E.Test.Actors/Reminders/IReminderActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.E2E.Test.Actors/State/IStateActor.cs b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs new file mode 100644 index 000000000..f19122102 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/State/IStateActor.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Threading.Tasks; +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors.State +{ + public interface IStateActor : IPingActor, IActor + { + Task GetState(string key); + + Task SetState(string key, string value, TimeSpan? ttl); + } +} diff --git a/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs index 5f2d5db86..58776fe28 100644 --- a/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs +++ b/test/Dapr.E2E.Test.App.ReentrantActor/Actors/ReentrantActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -67,4 +67,4 @@ private async Task UpdateState(bool isEnter, int callNumber) } } } -} \ No newline at end of file +} diff --git a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs index b08e483c2..57536377d 100644 --- a/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/ReminderActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.E2E.Test.App/Actors/StateActor.cs b/test/Dapr.E2E.Test.App/Actors/StateActor.cs new file mode 100644 index 000000000..71a952e0f --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/StateActor.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.State +{ + public class StateActor : Actor, IStateActor + { + public StateActor(ActorHost host) + : base(host) + { + } + + public Task Ping() + { + return Task.CompletedTask; + } + + public Task GetState(string key) + { + return this.StateManager.GetStateAsync(key); + } + + public Task SetState(string key, string value, TimeSpan? ttl) + { + if (ttl.HasValue) + { + return this.StateManager.SetStateAsync(key, value, ttl: ttl.Value); + } + return this.StateManager.SetStateAsync(key, value); + } + } +} diff --git a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs index bbe6cf7ae..4c6589965 100644 --- a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index 8207c5883..bd0de7b91 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -16,6 +16,7 @@ namespace Dapr.E2E.Test using Dapr.E2E.Test.Actors.Reentrancy; using Dapr.E2E.Test.Actors.Reminders; using Dapr.E2E.Test.Actors.Timers; + using Dapr.E2E.Test.Actors.State; using Dapr.E2E.Test.Actors.ExceptionTesting; using Dapr.E2E.Test.Actors.Serialization; using Dapr.E2E.Test.App.ErrorTesting; @@ -104,6 +105,7 @@ public void ConfigureServices(IServiceCollection services) options.Actors.RegisterActor(); options.Actors.RegisterActor(); options.Actors.RegisterActor(); + options.Actors.RegisterActor(); }); } diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs new file mode 100644 index 000000000..184a40448 --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.StateTests.cs @@ -0,0 +1,123 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +namespace Dapr.E2E.Test +{ + using System; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Dapr.Actors; + using Dapr.E2E.Test.Actors.State; + using Xunit; + + public partial class E2ETests : IAsyncLifetime + { + [Fact] + public async Task ActorCanSaveStateWithTTL() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + await Task.Delay(TimeSpan.FromSeconds(2.5)); + + // Assert key no longer exists. + await Assert.ThrowsAsync(() => proxy.GetState("key")); + + // Can create key again + await proxy.SetState("key", "new-value", null); + resp = await proxy.GetState("key"); + Assert.Equal("new-value", resp); + } + + [Fact] + public async Task ActorStateTTLOverridesExisting() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // TLL 4 seconds + await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); + + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 2 seconds + await Task.Delay(TimeSpan.FromSeconds(2)); + resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 4 seconds + await proxy.SetState("key", "value", TimeSpan.FromSeconds(4)); + + // TLL 2 seconds + await Task.Delay(TimeSpan.FromSeconds(2)); + resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + + // TLL 0 seconds + await Task.Delay(TimeSpan.FromSeconds(2.5)); + + // Assert key no longer exists. + await Assert.ThrowsAsync(() => proxy.GetState("key")); + } + + [Fact] + public async Task ActorStateTTLRemoveTTL() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var proxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "StateActor"); + + await WaitForActorRuntimeAsync(proxy, cts.Token); + + // Can remove TTL and then add again + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + await proxy.SetState("key", "value", null); + await Task.Delay(TimeSpan.FromSeconds(2)); + var resp = await proxy.GetState("key"); + Assert.Equal("value", resp); + await proxy.SetState("key", "value", TimeSpan.FromSeconds(2)); + await Task.Delay(TimeSpan.FromSeconds(2.5)); + await Assert.ThrowsAsync(() => proxy.GetState("key")); + } + + [Fact] + public async Task ActorStateBetweenProxies() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var actorId = ActorId.CreateRandom(); + var proxy1 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); + var proxy2 = this.ProxyFactory.CreateActorProxy(actorId, "StateActor"); + + await WaitForActorRuntimeAsync(proxy1, cts.Token); + + await proxy1.SetState("key", "value", TimeSpan.FromSeconds(2)); + var resp = await proxy1.GetState("key"); + Assert.Equal("value", resp); + resp = await proxy2.GetState("key"); + Assert.Equal("value", resp); + + await Task.Delay(TimeSpan.FromSeconds(2.5)); + await Assert.ThrowsAsync(() => proxy1.GetState("key")); + await Assert.ThrowsAsync(() => proxy2.GetState("key")); + } + } +} diff --git a/test/Dapr.E2E.Test/configuration/featureconfig.yaml b/test/Dapr.E2E.Test/configuration/featureconfig.yaml index 81ef1ecb1..4806c630f 100644 --- a/test/Dapr.E2E.Test/configuration/featureconfig.yaml +++ b/test/Dapr.E2E.Test/configuration/featureconfig.yaml @@ -12,3 +12,5 @@ spec: enabled: true - name: "proxy.grpc" enabled: true + - name: "ActorStateTTL" + enabled: true From d023a43ba4fd4cddb7aa2c0962cf786f01f58c24 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 26 Jan 2024 12:34:12 -0600 Subject: [PATCH 41/84] Added documentation detailing how serialization works using the DataContract serialization framework. (#1222) Signed-off-by: Whit Waldo --- .../dotnet-actors-serialization.md | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md new file mode 100644 index 000000000..abbeb437d --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-serialization.md @@ -0,0 +1,317 @@ +--- +type: docs +title: "Actor serialization in the .NET SDK" +linkTitle: "Actor serialization" +weight: 300000 +description: Necessary steps to serialize your types using remoted Actors in .NET +--- + +The Dapr actor package enables you to use Dapr virtual actors within a .NET application with strongly-typed remoting, but if you intend to send and receive strongly-typed data from your methods, there are a few key ground rules to understand. In this guide, you will learn how to configure your classes and records so they are properly serialized and deserialized at runtime. + +# Data Contract Serialization +When Dapr's virtual actors are invoked via the remoting proxy, your data is serialized using a serialization engine called the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) implemented by the [DataContractSerializer](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractserializer) class, which converts your C# types to and from XML documents. When sending or receiving primitives (like strings or ints), this serialization happens transparently and there's no requisite preparation needed on your part. However, when working with complex types such as those you create, there are some important rules to take into consideration so this process works smoothly. + +This serialization framework is not specific to Dapr and is separately maintained by the .NET team within the [.NET Github repository](https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContractSerializer.cs). + +## Serializable Types +There are several important considerations to keep in mind when using the Data Contract Serializer: + +- By default, all types, read/write properties (after construction) and fields marked as publicly visible are serialized +- All types must either expose a public parameterless constructor or be decorated with the DataContractAttribute attribute +- Init-only setters are only supported with the use of the DataContractAttribute attribute +- Read-only fields, properties without a Get and Set method and internal or properties with private Get and Set methods are ignored during serialization +- Serialization is supported for types that use other complex types that are not themselves marked with the DataContractAttribute attribute through the use of the KnownTypesAttribute attribute +- If a type is marked with the DataContractAttribute attribute, all members you wish to serialize and deserialize must be decorated with the DataMemberAttribute attribute as well or they'll be set to their default values + +## How does deserialization work? +The approach used for deserialization depends on whether or not the type is decorated with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. If this attribute isn't present, an instance of the type is created using the parameterless constructor. Each of the properties and fields are then mapped into the type using their respective setters and the instance is returned to the caller. + +If the type _is_ marked with `[DataContract]`, the serializer instead uses reflection to read the metadata of the type and determine which properties or fields should be included based on whether or not they're marked with the DataMemberAttribute attribute as it's performed on an opt-in basis. It then allocates an uninitialized object in memory (avoiding the use of any constructors, parameterless or not) and then sets the value directly on each mapped property or field, even if private or uses init-only setters. Serialization callbacks are invoked as applicable throughout this process and then the object is returned to the caller. + +Use of the serialization attributes is highly recommended as they grant more flexibility to override names and namespaces and generally use more of the modern C# functionality. While the default serializer can be relied on for primitive types, it's not recommended for any of your own types, whether they be classes, structs or records. It's recommended that if you decorate a type with the DataContractAttribute attribute, you also explicitly decorate each of the members you want to serialize or deserialize with the DataMemberAttribute attribute as well. + +### .NET Classes +Classes are fully supported in the Data Contract Serializer provided that that other rules detailed on this page and the [Data Contract Serializer](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types) documentation are also followed. + +The most important thing to remember here is that you must either have a public parameterless constructor or you must decorate it with the appropriate attributes. Let's review some examples to really clarify what will and won't work. + +In the following example, we present a simple class named Doodad. We don't provide an explicit constructor here, so the compiler will provide an default parameterless constructor. Because we're using [supported primitive types](###supported-primitive-types) (Guid, string and int32) and all our members have a public getter and setter, no attributes are required and we'll be able to use this class without issue when sending and receiving it from a Dapr actor method. + +```csharp +public class Doodad +{ + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +By default, this will serialize using the names of the members as used in the type and whatever values it was instantiated with: + +```xml + + a06ced64-4f42-48ad-84dd-46ae6a7e333d + DoodadName + 5 + +``` + +So let's tweak it - let's add our own constructor and only use init-only setters on the members. This will fail to serialize and deserialize not because of the use of the init-only setters, but because there's no parameterless constructors. + +```csharp +// WILL NOT SERIALIZE PROPERLY! +public class Doodad +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; init; } + public int Count { get; init; } +} +``` + +If we add a public parameterless constructor to the type, we're good to go and this will work without further annotations. + +```csharp +public class Doodad +{ + public Doodad() + { + } + + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +But what if we don't want to add this constructor? Perhaps you don't want your developers to accidentally create an instance of this Doodad using an unintended constructor. That's where the more flexible attributes are useful. If you decorate your type with a [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute, you can drop your parameterless constructor and it will work once again. + +```csharp +[DataContract] +public class Doodad +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +In the above example, we don't need to also use the [DataMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datamemberattribute) attributes because again, we're using [built-in primitives](###supported-primitive-types) that the serializer supports. But, we do get more flexibility if we use the attributes. From the DataContractAttribute attribute, we can specify our own XML namespace with the Namespace argument and, via the Name argument, change the name of the type as used when serialized into the XML document. + +It's a recommended practice to append the DataContractAttribute attribute to the type and the DataMemberAttribute attributes to all the members you want to serialize anyway - if they're not necessary and you're not changing the default values, they'll just be ignored, but they give you a mechanism to opt into serializing members that wouldn't otherwise have been included such as those marked as private or that are themselves complex types or collections. + +Note that if you do opt into serializing your private members, their values will be serialized into plain text - they can very well be viewed, intercepted and potentially manipulated based on how you're handing the data once serialized, so it's an important consideration whether you want to mark these members or not in your use case. + +In the following example, we'll look at using the attributes to change the serialized names of some of the members as well as introduce the [IgnoreDataMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.ignoredatamemberattribute) attribute. As the name indicates, this tells the serializer to skip this property even though it'd be otherwise eligible to serialize. Further, because I'm decorating the type with the DataContractAttribute attribute, it means that I can use init-only setters on the properties. + +```csharp +[DataContract(Name="Doodad")] +public class Doodad +{ + public Doodad(string name = "MyDoodad", int count = 5) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + [DataMember(Name = "id")] + public Guid Id { get; init; } + [IgnoreDataMember] + public string Name { get; init; } + [DataMember] + public int Count { get; init; } +} +``` + +When this is serialized, because we're changing the names of the serialized members, we can expect a new instance of Doodad using the default values this to be serialized as: + +```xml + + a06ced64-4f42-48ad-84dd-46ae6a7e333d + 5 + +``` + +#### Classes in C# 12 - Primary Constructors +C# 12 brought us primary constructors on classes. Use of a primary constructor means the compiler will be prevented from creating the default implicit parameterless constructor. While a primary constructor on a class doesn't generate any public properties, it does mean that if you pass this primary constructor any arguments or have non-primitive types in your class, you'll either need to specify your own parameterless constructor or use the serialization attributes. + +Here's an example where we're using the primary constructor to inject an ILogger to a field and add our own parameterless constructor without the need for any attributes. + +```csharp +public class Doodad(ILogger _logger) +{ + public Doodad() {} //Our parameterless constructor + + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + public Guid Id { get; set; } + public string Name { get; set; } + public int Count { get; set; } +} +``` + +And using our serialization attributes (again, opting for init-only setters since we're using the serialization attributes): + +```csharp +[DataContract] +public class Doodad(ILogger _logger) +{ + public Doodad(string name, int count) + { + Id = Guid.NewGuid(); + Name = name; + Count = count; + } + + [DataMember] + public Guid Id { get; init; } + [DataMember] + public string Name { get; init; } + [DataMember] + public int Count { get; init; } +} +``` + +### .NET Structs +Structs are supported by the Data Contract serializer provided that they are marked with the DataContractAttribute attribute and the members you wish to serialize are marked with the DataMemberAttribute attribute. Further, to support deserialization, the struct will also need to have a parameterless constructor. This works even if you define your own parameterless constructor as enabled in C# 10. + +```csharp +[DataContract] +public struct Doodad +{ + [DataMember] + public int Count { get; set; } +} +``` + +### .NET Records +Records were introduced in C# 9 and follow precisely the same rules as classes when it comes to serialization. We recommend that you should decorate all your records with the DataContractAttribute attribute and members you wish to serialize with DataMemberAttribute attributes so you don't experience any deserialization issues using this or other newer C# functionalities. Because record classes use init-only setters for properties by default and encourage the use of the primary constructor, applying these attributes to your types ensures that the serializer can properly otherwise accommodate your types as-is. + +Typically records are presented as a simple one-line statement using the new primary constructor concept: + +```csharp +public record Doodad(Guid Id, string Name, int Count); +``` + +This will throw an error encouraging the use of the serialization attributes as soon as you use it in a Dapr actor method invocation because there's no parameterless constructor available nor is it decorated with the aforementioned attributes. + +Here we add an explicit parameterless constructor and it won't throw an error, but none of the values will be set during deserialization since they're created with init-only setters. Because this doesn't use the DataContractAttribute attribute or the DataMemberAttribute attribute on any members, the serializer will be unable to map the target members correctly during deserialization. +```csharp +public record Doodad(Guid Id, string Name, int Count) +{ + public Doodad() {} +} +``` + +This approach does without the additional constructor and instead relies on the serialization attributes. Because we mark the type with the DataContractAttribute attribute and decorate each member with its own DataMemberAttribute attribute, the serialization engine will be able to map from the XML document to our type without issue. +```csharp +[DataContract] +public record Doodad( + [property: DataMember] Guid Id, + [property: DataMember] string Name, + [property: DataMember] int Count) +``` + +### Supported Primitive Types +There are several types built into .NET that are considered primitive and eligible for serialization without additional effort on the part of the developer: + +- [Byte](https://learn.microsoft.com/en-us/dotnet/api/system.byte) +- [SByte](https://learn.microsoft.com/en-us/dotnet/api/system.sbyte) +- [Int16](https://learn.microsoft.com/en-us/dotnet/api/system.int16) +- [Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32) +- [Int64](https://learn.microsoft.com/en-us/dotnet/api/system.int64) +- [UInt16](https://learn.microsoft.com/en-us/dotnet/api/system.uint16) +- [UInt32](https://learn.microsoft.com/en-us/dotnet/api/system.uint32) +- [UInt64](https://learn.microsoft.com/en-us/dotnet/api/system.uint64) +- [Single](https://learn.microsoft.com/en-us/dotnet/api/system.single) +- [Double](https://learn.microsoft.com/en-us/dotnet/api/system.double) +- [Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean) +- [Char](https://learn.microsoft.com/en-us/dotnet/api/system.char) +- [Decimal](https://learn.microsoft.com/en-us/dotnet/api/system.decimal) +- [Object](https://learn.microsoft.com/en-us/dotnet/api/system.object) +- [String](https://learn.microsoft.com/en-us/dotnet/api/system.string) + +There are additional types that aren't actually primitives but have similar built-in support: + +- [DateTime](https://learn.microsoft.com/en-us/dotnet/api/system.datetime) +- [TimeSpan](https://learn.microsoft.com/en-us/dotnet/api/system.timespan) +- [Guid](https://learn.microsoft.com/en-us/dotnet/api/system.guid) +- [Uri](https://learn.microsoft.com/en-us/dotnet/api/system.uri) +- [XmlQualifiedName](https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlqualifiedname) + +Again, if you want to pass these types around via your actor methods, no additional consideration is necessary as they'll be serialized and deserialized without issue. Further, types that are themselves marked with the (SerializeableAttribute)[https://learn.microsoft.com/en-us/dotnet/api/system.serializableattribute] attribute will be serialized. + +### Enumeration Types +Enumerations, including flag enumerations are serializable if appropriately marked. The enum members you wish to be serialized must be marked with the [EnumMemberAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.enummemberattribute) attribute in order to be serialized. Passing a custom value into the optional Value argument on this attribute will allow you to specify the value used for the member in the serialized document instead of having the serializer derive it from the name of the member. + +The enum type does not require that the type be decorated with the `DataContractAttribute` attribute - only that the members you wish to serialize be decorated with the `EnumMemberAttribute` attributes. + +```csharp +public enum Colors +{ + [EnumMember] + Red, + [EnumMember(Value="g")] + Green, + Blue, //Even if used by a type, this value will not be serialized as it's not decorated with the EnumMember attribute +} +``` + +### Collection Types +With regards to the data contact serializer, all collection types that implement the [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerable) interface including arays and generic collections are considered collections. Those types that implement [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.idictionary) or the generic [IDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.idictionary-2) are considered dictionary collections; all others are list collections. + +Not unlike other complex types, collection types must have a parameterless constructor available. Further, they must also have a method called Add so they can be properly serialized and deserialized. The types used by these collection types must themselves be marked with the `DataContractAttribute` attribute or otherwise be serializable as described throughout this document. + +### Data Contract Versioning +As the data contract serializer is only used in Dapr with respect to serializing the values in the .NET SDK to and from the Dapr actor instances via the proxy methods, there's little need to consider versioning of data contracts as the data isn't being persisted between application versions using the same serializer. For those interested in learning more about data contract versioning visit [here](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-versioning). + +### Known Types +Nesting your own complex types is easily accommodated by marking each of the types with the [DataContractAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.datacontractattribute) attribute. This informs the serializer as to how deserialization should be performed. +But what if you're working with polymorphic types and one of your members is a base class or interface with derived classes or other implementations? Here, you'll use the [KnownTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.knowntypeattribute) attribute to give a hint to the serializer about how to proceed. + +When you apply the [KnownTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.knowntypeattribute) attribute to a type, you are informing the data contract serializer about what subtypes it might encounter allowing it to properly handle the serialization and deserialization of these types, even when the actual type at runtime is different from the declared type. + +```chsarp +[DataContract] +[KnownType(typeof(DerivedClass))] +public class BaseClass +{ + //Members of the base class +} + +[DataContract] +public class DerivedClass : BaseClass +{ + //Additional members of the derived class +} +``` + +In this example, the `BaseClass` is marked with `[KnownType(typeof(DerivedClass))]` which tells the data contract serializer that `DerivedClass` is a possible implementation of `BaseClass` that it may need to serialize or deserialize. Without this attribute, the serialize would not be aware of the `DerivedClass` when it encounters an instance of `BaseClass` that is actually of type `DerivedClass` and this could lead to a serialization exception because the serializer would not know how to handle the derived type. By specifying all possible derived types as known types, you ensure that the serializer can process the type and its members correctly. + +For more information and examples about using `[KnownType]`, please refer to the [official documentation](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/data-contract-known-types). \ No newline at end of file From 348d1430ba3fa38c3409c2ab58aa7a62d37399f7 Mon Sep 17 00:00:00 2001 From: Remco Blok Date: Wed, 31 Jan 2024 05:33:15 +0000 Subject: [PATCH 42/84] Weakly typed actor polymorphic and null responses (#1214) Signed-off-by: Remco Blok Co-authored-by: Remco Blok Co-authored-by: Phillip Hoff --- src/Dapr.Actors/Runtime/ActorManager.cs | 8 ++- .../WeaklyTypedTesting/DerivedResponse.cs | 20 ++++++ .../IWeaklyTypedTestingActor.cs | 25 +++++++ .../WeaklyTypedTesting/ResponseBase.cs | 25 +++++++ .../Actors/WeaklyTypedTestingActor.cs | 47 +++++++++++++ test/Dapr.E2E.Test.App/Startup.cs | 2 + .../Actors/E2ETests.WeaklyTypedTests.cs | 68 +++++++++++++++++++ 7 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs create mode 100644 test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs create mode 100644 test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs create mode 100644 test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs create mode 100644 test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs diff --git a/src/Dapr.Actors/Runtime/ActorManager.cs b/src/Dapr.Actors/Runtime/ActorManager.cs index d766cd485..80049d65f 100644 --- a/src/Dapr.Actors/Runtime/ActorManager.cs +++ b/src/Dapr.Actors/Runtime/ActorManager.cs @@ -186,7 +186,13 @@ async Task RequestFunc(Actor actor, CancellationToken ct) // Serialize result if it has result (return type was not just Task.) if (methodInfo.ReturnType.Name != typeof(Task).Name) { - await JsonSerializer.SerializeAsync(responseBodyStream, result, result.GetType(), jsonSerializerOptions); +#if NET7_0_OR_GREATER + var resultType = methodInfo.ReturnType.GenericTypeArguments[0]; + await JsonSerializer.SerializeAsync(responseBodyStream, result, resultType, jsonSerializerOptions); +#else + await JsonSerializer.SerializeAsync(responseBodyStream, result, jsonSerializerOptions); +#endif + } } diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs new file mode 100644 index 000000000..c11f547e7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/DerivedResponse.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public class DerivedResponse : ResponseBase + { + public string DerivedProperty { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs new file mode 100644 index 000000000..2c5d1e82d --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/IWeaklyTypedTestingActor.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Threading.Tasks; +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public interface IWeaklyTypedTestingActor : IPingActor, IActor + { + Task GetPolymorphicResponse(); + + Task GetNullResponse(); + } +} diff --git a/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs new file mode 100644 index 000000000..45736d808 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors/WeaklyTypedTesting/ResponseBase.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ +#if NET7_0_OR_GREATER + [JsonDerivedType(typeof(DerivedResponse), typeDiscriminator: nameof(DerivedResponse))] +#endif + public class ResponseBase + { + public string BasePropeprty { get; set; } + } +} diff --git a/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs new file mode 100644 index 000000000..3cbe20bad --- /dev/null +++ b/test/Dapr.E2E.Test.App/Actors/WeaklyTypedTestingActor.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Threading.Tasks; +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.WeaklyTypedTesting +{ + public class WeaklyTypedTestingActor : Actor, IWeaklyTypedTestingActor + { + public WeaklyTypedTestingActor(ActorHost host) + : base(host) + { + } + + public Task GetNullResponse() + { + return Task.FromResult(null); + } + + public Task GetPolymorphicResponse() + { + var response = new DerivedResponse + { + BasePropeprty = "Base property value", + DerivedProperty = "Derived property value" + }; + + return Task.FromResult(response); + } + + public Task Ping() + { + return Task.CompletedTask; + } + } +} diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index bd0de7b91..bfca60f91 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -19,6 +19,7 @@ namespace Dapr.E2E.Test using Dapr.E2E.Test.Actors.State; using Dapr.E2E.Test.Actors.ExceptionTesting; using Dapr.E2E.Test.Actors.Serialization; + using Dapr.E2E.Test.Actors.WeaklyTypedTesting; using Dapr.E2E.Test.App.ErrorTesting; using Dapr.Workflow; using Microsoft.AspNetCore.Authentication; @@ -106,6 +107,7 @@ public void ConfigureServices(IServiceCollection services) options.Actors.RegisterActor(); options.Actors.RegisterActor(); options.Actors.RegisterActor(); + options.Actors.RegisterActor(); }); } diff --git a/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs new file mode 100644 index 000000000..b250d731e --- /dev/null +++ b/test/Dapr.E2E.Test/Actors/E2ETests.WeaklyTypedTests.cs @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ +namespace Dapr.E2E.Test +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Dapr.Actors; + using Dapr.E2E.Test.Actors.WeaklyTypedTesting; + using FluentAssertions; + using Xunit; + + public partial class E2ETests : IAsyncLifetime + { +#if NET8_0_OR_GREATER + [Fact] + public async Task WeaklyTypedActorCanReturnPolymorphicResponse() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); + + result.Should().BeOfType().Which.DerivedProperty.Should().NotBeNullOrWhiteSpace(); + } +#else + [Fact] + public async Task WeaklyTypedActorCanReturnDerivedResponse() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetPolymorphicResponse)); + + result.Should().BeOfType().Which.DerivedProperty.Should().NotBeNullOrWhiteSpace(); + } +#endif + [Fact] + public async Task WeaklyTypedActorCanReturnNullResponse() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var pingProxy = this.ProxyFactory.CreateActorProxy(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + var proxy = this.ProxyFactory.Create(ActorId.CreateRandom(), "WeaklyTypedTestingActor"); + + await WaitForActorRuntimeAsync(pingProxy, cts.Token); + + var result = await proxy.InvokeMethodAsync(nameof(IWeaklyTypedTestingActor.GetNullResponse)); + + result.Should().BeNull(); + } + } +} From ca2fab2567d8c25b3deacd00eacabed4579a4aa4 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 14 Feb 2024 14:29:08 -0500 Subject: [PATCH 43/84] Implementing Cryptography building block in .NET (#1217) * Added method to DaprClient and GRPC implementation to call cryptography proto endpoints Signed-off-by: Whit Waldo * First pass at implementing all exposed Cryptography methods on Go interface Signed-off-by: Whit Waldo * Added examples for Cryptography block Signed-off-by: Whit Waldo * Added missing copyright statements Signed-off-by: Whit Waldo * Updated to properly support Crypto API this time Signed-off-by: Whit Waldo * Added copyright statements Signed-off-by: Whit Waldo * Removed deprecated examples as the subtle APIs are presently disabled Signed-off-by: Whit Waldo * Updated example to reflect new API shape Signed-off-by: Whit Waldo * Updated example and readme Signed-off-by: Whit Waldo * Added overloads for encrypting/decrypting streams instead of just fixed byte arrays. Added example demonstrating the same encrypting a file via a FileStream and decrypting from a MemoryStream. Signed-off-by: Whit Waldo * Added some unit tests to pair with the implementation Signed-off-by: Whit Waldo * Added null check for the stream argument Signed-off-by: Whit Waldo * Changed case of the arguments as they should read "plaintext" and not "plainText" Signed-off-by: Whit Waldo * Reduced number of encryption implementations by just wrapping byte array into memory stream Signed-off-by: Whit Waldo * Constrainted returned member types per review suggestion Signed-off-by: Whit Waldo * Updated methods to use ReadOnlyMemory instead of byte[] - updated implementations to use low-allocation spans where possible (though ToArray is necessary to wrap with MemoryStream). Signed-off-by: Whit Waldo * Updated to use encryption/decryption options instead of lots of method overload variations. Simplified gRPC implementation to use fewer methods. Applied argument name updates applied previously (plainText -> plaintext). Signed-off-by: Whit Waldo * Updated tests Signed-off-by: Whit Waldo * Removed unused reference Signed-off-by: Whit Waldo * Updated examples to reflect new method shapes. Downgraded package to .net 6 instead of .net 8 per review suggestion. Signed-off-by: Whit Waldo * Updated to reflect non-aliased values per review suggestion Signed-off-by: Whit Waldo * Update to ensure that both send/receive streams run at the same time instead of sequentially. Signed-off-by: Whit Waldo * Updated to support streamed results in addition to fixed byte arrays. Refactored implementation to minimize duplicative code. Signed-off-by: Whit Waldo * Updated example to fix compile issue Signed-off-by: Whit Waldo * Removed encrypt/decrypt methods that accepted streams and returned ReadOnlyMemory. Marked implementations that use this on the gRPC class as private instead. Signed-off-by: Whit Waldo * Added missing Obsolete attributes on Encrypt/Decrypt methods. Added overloads on decrypt methods that do not require a DecryptionOptions to be passed in. Signed-off-by: Whit Waldo * Updated encrypt/decrypt options so the streaming block size no longer uses a uint. Added validation in its place to ensure the value provided is never less than or equal to 0. Signed-off-by: Whit Waldo * Updated how validation works in the options to accommodate lack of the shorter variation in .NET 6 Signed-off-by: Whit Waldo * Updated names of encrypt/decrypt streaming methods so everything uses just EncryptAsync or DecryptAsync Signed-off-by: Whit Waldo * Fixed regression that would have prevented data from being sent entirely to the sidecar. Also simplified operation per suggestion in review. Signed-off-by: Whit Waldo * Updated examples to reflect changed API Signed-off-by: Whit Waldo * Updated so IAsyncEnumerable methods (encrypt and decrypt) return IAsyncEnumerable> instead of IAsyncEnumerable. Signed-off-by: Whit Waldo * Updated example to reflect change from IAsyncEnumerable to IAsyncEnumerable> Signed-off-by: Whit Waldo * Avoiding allocation by using MemoryMarshal instead of .ToArray() to create MemoryStream from ReadOnlyMemory. Signed-off-by: Whit Waldo * Performance updates to minimize unnecessary byte array copies and eliminate unnecessary allocations. Signed-off-by: Whit Waldo * Removed unnecessary return from SendPlaintextStreamAsync and SendCiphertextStreamAsync methods Signed-off-by: Whit Waldo * Updated exception text to be more specific as to what's wrong with the input value. Signed-off-by: Whit Waldo * Minor tweak to prefer using using a Memory Signed-off-by: Whit Waldo * Deduplicated some of the Decrypt methods, simplifying the implementation Signed-off-by: Whit Waldo * Eliminated duplicate encryption method, simplifying implementation Signed-off-by: Whit Waldo * Updated to eliminate an unnecessary `await` and `async foreach`. Signed-off-by: Whit Waldo * Updated stream example to reflect the changes to the API shape Signed-off-by: Whit Waldo * Added notes about operations with stream-based data Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo --- all.sln | 11 + .../Components/azurekeyvault.yaml | 25 + .../Components/env-secretstore.yaml | 7 + .../Client/Cryptography/Cryptography.csproj | 25 + examples/Client/Cryptography/Example.cs | 22 + .../EncryptDecryptFileStreamExample.cs | 77 +++ .../Examples/EncryptDecryptStringExample.cs | 47 ++ examples/Client/Cryptography/Program.cs | 47 ++ examples/Client/Cryptography/README.md | 92 ++++ examples/Client/Cryptography/file.txt | 26 + src/Dapr.Client/CryptographyEnums.cs | 76 +++ src/Dapr.Client/CryptographyOptions.cs | 80 +++ src/Dapr.Client/DaprClient.cs | 283 +++++++++- src/Dapr.Client/DaprClientGrpc.cs | 498 +++++++++++++++++- src/Dapr.Client/EnumExtensions.cs | 38 ++ test/Dapr.Client.Test/CryptographyApiTest.cs | 95 ++++ test/Dapr.Client.Test/EnumExtensionTest.cs | 38 ++ 17 files changed, 1485 insertions(+), 2 deletions(-) create mode 100644 examples/Client/Cryptography/Components/azurekeyvault.yaml create mode 100644 examples/Client/Cryptography/Components/env-secretstore.yaml create mode 100644 examples/Client/Cryptography/Cryptography.csproj create mode 100644 examples/Client/Cryptography/Example.cs create mode 100644 examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs create mode 100644 examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs create mode 100644 examples/Client/Cryptography/Program.cs create mode 100644 examples/Client/Cryptography/README.md create mode 100644 examples/Client/Cryptography/file.txt create mode 100644 src/Dapr.Client/CryptographyEnums.cs create mode 100644 src/Dapr.Client/CryptographyOptions.cs create mode 100644 src/Dapr.Client/EnumExtensions.cs create mode 100644 test/Dapr.Client.Test/CryptographyApiTest.cs create mode 100644 test/Dapr.Client.Test/EnumExtensionTest.cs diff --git a/all.sln b/all.sln index 47fc9098c..bf0b87352 100644 --- a/all.sln +++ b/all.sln @@ -104,6 +104,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "examples\Workflow\WorkflowUnitTest\WorkflowUnitTest.csproj", "{8CA09061-2BEF-4506-A763-07062D2BD6AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -248,6 +250,14 @@ Global {DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.Build.0 = Release|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -293,6 +303,7 @@ Global {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Client/Cryptography/Components/azurekeyvault.yaml b/examples/Client/Cryptography/Components/azurekeyvault.yaml new file mode 100644 index 000000000..5932e0bc8 --- /dev/null +++ b/examples/Client/Cryptography/Components/azurekeyvault.yaml @@ -0,0 +1,25 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azurekeyvault +spec: + type: crypto.azure.keyvault + metadata: + - name: vaultName + value: "" + - name: azureEnvironment + value: AZUREPUBLICCLOUD + - name: azureTenantId + secretKeyRef: + name: read_azure_tenant_id + key: read_azure_tenant_id + - name: azureClientId + secretKeyRef: + name: read_azure_client_id + key: read_azure_client_id + - name: azureClientSecret + secretKeyRef: + name: read_azure_client_secret + key: read_azure_client_secret +auth: + secureStore: envvar-secret-store \ No newline at end of file diff --git a/examples/Client/Cryptography/Components/env-secretstore.yaml b/examples/Client/Cryptography/Components/env-secretstore.yaml new file mode 100644 index 000000000..fb191414d --- /dev/null +++ b/examples/Client/Cryptography/Components/env-secretstore.yaml @@ -0,0 +1,7 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store +spec: + type: secretstores.local.env + version: v1 \ No newline at end of file diff --git a/examples/Client/Cryptography/Cryptography.csproj b/examples/Client/Cryptography/Cryptography.csproj new file mode 100644 index 000000000..525c38562 --- /dev/null +++ b/examples/Client/Cryptography/Cryptography.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + latest + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/examples/Client/Cryptography/Example.cs b/examples/Client/Cryptography/Example.cs new file mode 100644 index 000000000..2c2d41626 --- /dev/null +++ b/examples/Client/Cryptography/Example.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Cryptography +{ + internal abstract class Example + { + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs new file mode 100644 index 000000000..aa9c404a7 --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Buffers; +using Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptFileStreamExample : Example + { + public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + const string componentName = "azurekeyvault"; // Change this to match the name of the component containing your vault + const string keyName = "myKey"; + + // The name of the file we're using as an example + const string fileName = "file.txt"; + + Console.WriteLine("Original file contents:"); + foreach (var line in await File.ReadAllLinesAsync(fileName, cancellationToken)) + { + Console.WriteLine(line); + } + Console.WriteLine(); + + //Encrypt from a file stream and buffer the resulting bytes to an in-memory buffer + await using var encryptFs = new FileStream(fileName, FileMode.Open); + + var bufferedEncryptedBytes = new ArrayBufferWriter(); + await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) + .WithCancellation(cancellationToken)) + { + bufferedEncryptedBytes.Write(bytes.Span); + } + + Console.WriteLine($"Encrypted bytes: {Convert.ToBase64String(bufferedEncryptedBytes.GetSpan())}"); + Console.WriteLine(); + + //We'll write to a temporary file via a FileStream + var tempDecryptedFile = Path.GetTempFileName(); + await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); + + //We'll stream the decrypted bytes from a MemoryStream into the above temporary file + await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); + await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName, + cancellationToken)).WithCancellation(cancellationToken)) + { + decryptFs.Write(result.Span); + } + + decryptFs.Close(); + + //Let's confirm the value as written to the file + var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); + Console.WriteLine($"Decrypted value: "); + Console.WriteLine(decryptedValue); + + //And some cleanup to delete our temp file + File.Delete(tempDecryptedFile); + } + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs new file mode 100644 index 000000000..a37ca1b8b --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; +using Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptStringExample : Example + { + public override string DisplayName => "Using Cryptography to encrypt and decrypt a string"; + + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + const string componentName = "azurekeyvault"; //Change this to match the name of the component containing your vault + const string keyName = "myKey"; //Change this to match the name of the key in your Vault + + + const string plaintextStr = "This is the value we're going to encrypt today"; + Console.WriteLine($"Original string value: '{plaintextStr}'"); + + //Encrypt the string + var plaintextBytes = Encoding.UTF8.GetBytes(plaintextStr); + var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + cancellationToken); + + Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); + + //Decrypt the string + var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, new DecryptionOptions(), cancellationToken); + Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); + } + } +} diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs new file mode 100644 index 000000000..74e3c7f48 --- /dev/null +++ b/examples/Client/Cryptography/Program.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Cryptography; +using Cryptography.Examples; + +namespace Samples.Client +{ + class Program + { + private static readonly Example[] Examples = new Example[] + { + new EncryptDecryptStringExample(), + new EncryptDecryptFileStreamExample() + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) + { + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); + + await Examples[index].RunAsync(cts.Token); + return 0; + } + + Console.WriteLine("Hello, please choose a sample to run:"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 1; + } + } +} diff --git a/examples/Client/Cryptography/README.md b/examples/Client/Cryptography/README.md new file mode 100644 index 000000000..c0c884369 --- /dev/null +++ b/examples/Client/Cryptography/README.md @@ -0,0 +1,92 @@ +# Dapr .NET SDK Cryptography example + +## Prerequisites + +- [.NET 8+](https://dotnet.microsoft.com/download) installed +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/installation) +- [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) +- [Azure Key Vault instance](https://learn.microsoft.com/en-us/azure/key-vault/general/quick-create-portal) +- [Entra Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) + +### Service Principal/Environment Variables Setup +In your Azure portal, open Microsoft Entra ID and click `App Registrations`. Click the button at the top to create a new registration. Select a name for your service principal +and click register, noting this name for later. + +Once the registration is completed, open it from the list and select Certificates & Secrets from the left navigation. Select "Client secrets" from the page body (middle column) +and click the button to add a new client secret giving it an optional description and changing the expiry date as you desire. Click Add to create the secret. Record the secret +value it shows you - it will not be shown to you again without creating another client secret. + +Click Overview from the left navigation and record the "Application (client) ID" and the "Directory (tenant) ID" values. + +On your computer (assuming Windows), open your start menu and type "Environment Variables". An option should appear named "Edit the system environment variables". Select this +and your System Properties window will open. Click the "Environment Variables" button in the bottom and said window will appear. Click the "New..." button under System variables +to add the requisite service principal values to your environment variables. You can change these names as to want by updating the `./Components/azurekeyvault.yaml` names, but for now +configure as follows: + +| Variable Name | Value | +|--|--| +| read_azure_client_id | Paste the value from your app registration overview for "Application (client) ID" | +| read_azure_client_secret | Paste the value of the client secret you generated for your app registration | +| read_azure_tenant_id | Paste the valeu from your app registration overview for "Directory (tenant) ID" | + +Click OK to save your environment variables and to close your System Properties window. You may need to close restart your command line tool for it to recognize the new values. + +### Azure Key Vault Setup + +This example is implemented using the Azure Key Vault and will not work without it. Assuming you have a Key Vault instance configured, ensure that +you have the `Key Vault Crypto Officer` role assigned to yourself as you'll need to in order to generate a new key in the instance. After selecting Keys +under the Objects header, click the `Generate/Import` button at the top of the instance panel. + +Under options, select `Generate` and name your key. This example is pre-configured to assume a key name of 'myKey', but feel free to change this (but also update the name in the example +you wish to run). The other default options are fine for our purposes, so click Create at the bottom and if you've got the appropriate roles, it will show up in the list of Keys. + +Update your `./Components/azurekeyvault.yaml` file with the name of your Key Vault under `vaultName` where it currently reads "changeMe". This sample assumes authentication +via a service principal, so you might also need to set this up. + +Back in the Azure Portal, assign at least the `Key Vault Crypto User` role to the service principal you previously created in the last step. Do this by clicking +`Access Control (IAM)` from the left navigation, clicking "Add" from the top and clicking "Add Role Assignment". Select `Key Vault Crypto User` from the list and click the Next +button. Ensuring that the "User, group or service principal" option is selected, click the "Select members" link and search for the name of the app registration you created. Click +Add to add this service principal to the list of members for the new role assignment and click Review + Assign twice to assign the role. This will take effect within a few seconds +or minutes. This step ensures that while Dapr can authenticate as your service principal, that it also has permission to access and use the key in your Key Vault. + +## Running the example + +To run the sample locally, run this command in the DaprClient directory: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Running the following command will output a list of the samples included: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Press Ctrl+C to exit, and then run the command again and provide a sample number to run the samples. + +For example, run this command to run the first sample from the list produced earlier (the 0th example): + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run 0 +``` + +## Encryption/Decryption with strings +See [EncryptDecryptStringExample.cs](./EncryptDecryptStringExample.cs) for an example of using `DaprClient` for basic +string-based encryption and decryption operations as performed against UTF-8 encoded byte arrays. + +## Encryption/Decryption with streams +See [EncryptDecryptFileStreamExample.cs](./EncryptDecryptFileStreamExample.cs) for an example of using `DaprClient` +to perform an encrypt and decrypt operation against a stream of data. In the example, we stream a local file to the +sidecar to encrypt and write the result (as it's streamed back) to an in-memory buffer. Once the operation fully +completes, we perform the decrypt operation against this in-memory buffer and write the decrypted result back out to a +temporary file. + +In either operation, rather than load the entire stream into memory and send all at once to the +sidecar as we do in the other string-based example (as this might cause you to run out of memory either on the +node the app is running on or do the same to the sidecar itself), this example instead breaks the input stream into +more manageable 4KB chunks (a value you can override via the `EncryptionOptions` or `DecryptionOptions` parameters +respectively up to 64KB. Further, rather than waiting for the entire stream to send to the sidecar before the +encryption operation proceeds, it immediately works to process the sidecar response, continuing to minimize resource +usage. diff --git a/examples/Client/Cryptography/file.txt b/examples/Client/Cryptography/file.txt new file mode 100644 index 000000000..9e8638939 --- /dev/null +++ b/examples/Client/Cryptography/file.txt @@ -0,0 +1,26 @@ +# The Road Not Taken +## By Robert Lee Frost + +Two roads diverged in a yellow wood, +And sorry I could not travel both +And be one traveler, long I stood +And looked down one as far as I could +To where it bent in the undergrowth; + +Then took the other, as just as fair +And having perhaps the better claim, +Because it was grassy and wanted wear; +Though as for that, the passing there +Had worn them really about the same, + +And both that morning equally lay +In leaves no step had trodden black +Oh, I kept the first for another day! +Yet knowing how way leads on to way, +I doubted if I should ever come back. + +I shall be telling this with a sigh +Somewhere ages and ages hence: +Two roads diverged in a wood, and I, +I took the one less traveled by, +And that has made all the difference. \ No newline at end of file diff --git a/src/Dapr.Client/CryptographyEnums.cs b/src/Dapr.Client/CryptographyEnums.cs new file mode 100644 index 000000000..f5955b389 --- /dev/null +++ b/src/Dapr.Client/CryptographyEnums.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.Serialization; + +namespace Dapr.Client +{ + /// + /// The cipher used for data encryption operations. + /// + public enum DataEncryptionCipher + { + /// + /// The default data encryption cipher used, this represents AES GCM. + /// + [EnumMember(Value = "aes-gcm")] + AesGcm, + /// + /// Represents the ChaCha20-Poly1305 data encryption cipher. + /// + [EnumMember(Value = "chacha20-poly1305")] + ChaCha20Poly1305 + }; + + /// + /// The algorithm used for key wrapping cryptographic operations. + /// + public enum KeyWrapAlgorithm + { + /// + /// Represents the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + Aes, + /// + /// An alias for the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + A256kw, + /// + /// Represents the AES 128 CBC key wrap algorithm. + /// + [EnumMember(Value="A128CBC")] + A128cbc, + /// + /// Represents the AES 192 CBC key wrap algorithm. + /// + [EnumMember(Value="A192CBC")] + A192cbc, + /// + /// Represents the AES 256 CBC key wrap algorithm. + /// + [EnumMember(Value="A256CBC")] + A256cbc, + /// + /// Represents the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + Rsa, + /// + /// An alias for the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + RsaOaep256 //Alias for RSA + } +} diff --git a/src/Dapr.Client/CryptographyOptions.cs b/src/Dapr.Client/CryptographyOptions.cs new file mode 100644 index 000000000..ae94a8f2f --- /dev/null +++ b/src/Dapr.Client/CryptographyOptions.cs @@ -0,0 +1,80 @@ +#nullable enable +using System; + +namespace Dapr.Client +{ + /// + /// A collection of options used to configure how encryption cryptographic operations are performed. + /// + public class EncryptionOptions + { + /// + /// Creates a new instance of the . + /// + /// + public EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) + { + KeyWrapAlgorithm = keyWrapAlgorithm; + } + + /// + /// The name of the algorithm used to wrap the encryption key. + /// + public KeyWrapAlgorithm KeyWrapAlgorithm { get; set; } + + private int streamingBlockSizeInBytes = 4 * 1024; // 4 KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + /// + /// This defaults to 4KB and generally should not exceed 64KB. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + streamingBlockSizeInBytes = value; + } + } + + /// + /// The optional name (and optionally a version) of the key specified to use during decryption. + /// + public string? DecryptionKeyName { get; set; } = null; + + /// + /// The name of the cipher to use for the encryption operation. + /// + public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; + } + + /// + /// A collection fo options used to configure how decryption cryptographic operations are performed. + /// + public class DecryptionOptions + { + private int streamingBlockSizeInBytes = 4 * 1024; // 4KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + streamingBlockSizeInBytes = value; + } + } + } +} diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 361ac54bc..20c37d9e7 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -3,7 +3,7 @@ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -939,6 +940,286 @@ public abstract Task UnsubscribeConfiguration( string id, CancellationToken cancellationToken = default); + #region Cryptography + + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> EncryptAsync(string vaultResourceName, Stream plaintextStream, string keyName, + EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions options, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, DecryptionOptions options, CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, CancellationToken cancellationToken = default); + + #endregion + + #region Cryptography - Subtle API + + ///// + ///// Retrieves the value of the specified key from the vault. + ///// + ///// The name of the vault resource used by the operation. + ///// The name of the key to retrieve the value of. + ///// The format to use for the key result. + ///// A that can be used to cancel the operation. + ///// The name (and possibly version as name/version) of the key and its public key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default); + + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The array of encrypted bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// A that can be used to cancel the operation. + ///// The array of encrypted bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // CancellationToken cancellationToken = default) => + // await EncryptAsync(vaultResourceName, plainTextBytes, algorithm, keyName, nonce, Array.Empty(), + // cancellationToken); + + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// Any associated data when using AEAD ciphers. + ///// The array of plaintext bytes. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// The array of plaintext bytes. + //[Obsolete( + // "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, CancellationToken cancellationToken = default) => + // await DecryptAsync(vaultResourceName, cipherTextBytes, algorithm, keyName, nonce, tag, Array.Empty(), cancellationToken); + + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the wrapped plain-text key and the authentication tag, if applicable. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, byte[] nonce, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key and the authentication tag, if applicable. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, + // byte[] nonce, CancellationToken cancellationToken = default) => await WrapKeyAsync(vaultResourceName, plainTextKey, + // keyName, algorithm, nonce, Array.Empty(), cancellationToken); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, byte[] tag, + // CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + + ///// + ///// Creates a signature of a digest value. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest value to create the signature for. + ///// The algorithm used to create the signature. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the signature. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, + // CancellationToken cancellationToken = default); + + ///// + ///// Validates a signature. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest to validate the signature with. + ///// The signature to validate. + ///// The algorithm to validate the signature with. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// True if the signature verification is successful; otherwise false. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public abstract Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, string algorithm, string keyName, + // CancellationToken cancellationToken = default); + + #endregion + /// /// Attempt to lock the given resourceId with response indicating success. /// diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 75df09323..9c99b9eee 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -3,7 +3,7 @@ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,10 +14,14 @@ namespace Dapr.Client { using System; + using System.Buffers; using System.Collections.Generic; + using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Json; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -1373,6 +1377,498 @@ public override async Task UnsubscribeConfigur var resp = await client.UnsubscribeConfigurationAsync(request, options); return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); } + + #endregion + + #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) + { + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) + { + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + 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(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; + } + + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.EncryptAlpha1(options); + + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( + //Stream the plaintext data to the sidecar in chunks + SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, encryptRequestOptions, cancellationToken), + //At the same time, retrieve the encrypted response from the sidecar + receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); + } + + /// + /// 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; + + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != + 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + + //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)) + { + yield return encryptResponse.Payload.Data.Memory; + } + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, + DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, + KeyName = keyName + }; + + var options = CreateCallOptions(headers: null, cancellationToken); + var duplexStream = client.DecryptAlpha1(options); + + //Run both operations at the same time, but return the output of the streaming values coming from the operation + var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); + return await Task.WhenAll( + //Stream the ciphertext data to the sidecar in chunks + SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes, + duplexStream, decryptRequestOptions, cancellationToken), + //At the same time, retrieve the decrypted response from the sidecar + receiveResult) + //Return only the result of the `RetrieveEncryptedStreamAsync` method + .ContinueWith(t => receiveResult.Result, cancellationToken); + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); + + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + int streamingBlockSizeInBytes, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions decryptRequestOptions, + CancellationToken cancellationToken) + { + //Start with passing the metadata about the decryption request itself in the first message + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); + + //Send the ciphertext bytes in blocks in subsequent messages + await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) + { + var buffer = new byte[streamingBlockSizeInBytes]; + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), cancellationToken)) != 0) + { + await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), + Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return decryptResponse.Payload.Data.Memory; + } + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) + { + var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), + keyName, decryptionOptions, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + } + + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync(string vaultResourceName, + ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public override async Task VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} + + #endregion + + #endregion #region Distributed Lock API diff --git a/src/Dapr.Client/EnumExtensions.cs b/src/Dapr.Client/EnumExtensions.cs new file mode 100644 index 000000000..6b058ca77 --- /dev/null +++ b/src/Dapr.Client/EnumExtensions.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.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 + { + 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(); + } + } +} diff --git a/test/Dapr.Client.Test/CryptographyApiTest.cs b/test/Dapr.Client.Test/CryptographyApiTest.cs new file mode 100644 index 000000000..a7d57a096 --- /dev/null +++ b/test/Dapr.Client.Test/CryptographyApiTest.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Dapr.Client.Test +{ + public class CryptographyApiTest + { + [Fact] + public async Task EncryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + (ReadOnlyMemory)Array.Empty(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync( "myVault", + (ReadOnlyMemory) Array.Empty(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync("myVault", + (Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + Array.Empty(), "myKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + Array.Empty(), keyName, new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None)); + } + } +} diff --git a/test/Dapr.Client.Test/EnumExtensionTest.cs b/test/Dapr.Client.Test/EnumExtensionTest.cs new file mode 100644 index 000000000..be78c3861 --- /dev/null +++ b/test/Dapr.Client.Test/EnumExtensionTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using Xunit; + +namespace Dapr.Client.Test +{ + 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 + } +} From 817b60d9c67337681f968172cff5a6fce7f68f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Chauss=C3=A9?= Date: Fri, 16 Feb 2024 18:02:22 +0100 Subject: [PATCH 44/84] Handle the case where appid contains at least one upperletter (#1233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Handle the case where appid can contain some uppercases Signed-off-by: Nicolas Chaussé Signed-off-by: TWEESTY Signed-off-by: Nicolas Chaussé * Add one test sample Signed-off-by: Nicolas Chaussé Signed-off-by: TWEESTY Signed-off-by: Nicolas Chaussé * Optimization in order to not add some overhead time for the "normal" use case Signed-off-by: TWEESTY Signed-off-by: Nicolas Chaussé * Change comment which was false Signed-off-by: TWEESTY Signed-off-by: Nicolas Chaussé * Remove the breaking change Signed-off-by: TWEESTY Signed-off-by: Nicolas Chaussé * Simplify according to the review Signed-off-by: Nicolas Chaussé --------- Signed-off-by: Nicolas Chaussé Signed-off-by: TWEESTY --- src/Dapr.Client/DaprClient.cs | 8 +- src/Dapr.Client/InvocationHandler.cs | 20 ++++- test/Dapr.Client.Test/DaprClientTest.cs | 2 +- .../InvocationHandlerTests.cs | 86 ++++++++++++++++--- 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 20c37d9e7..7f094804f 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -63,7 +63,8 @@ public abstract class DaprClient : IDisposable /// /// /// An optional app-id. If specified, the app-id will be configured as the value of - /// so that relative URIs can be used. + /// so that relative URIs can be used. It is mandatory to set this parameter if your app-id contains at least one upper letter. + /// If some requests use absolute URL with an app-id which contains at least one upper letter, it will not work, the workaround is to create one HttpClient for each app-id with the app-ip parameter set. /// /// The HTTP endpoint of the Dapr process to use for service invocation calls. /// The token to be added to all request headers to Dapr runtime. @@ -80,7 +81,8 @@ public static HttpClient CreateInvokeHttpClient(string appId = null, string dapr var handler = new InvocationHandler() { InnerHandler = new HttpClientHandler(), - DaprApiToken = daprApiToken + DaprApiToken = daprApiToken, + DefaultAppId = appId, }; if (daprEndpoint is string) @@ -210,7 +212,7 @@ public abstract Task PublishEventAsync( string topicName, Dictionary metadata, CancellationToken cancellationToken = default); - + /// /// // Bulk Publishes multiple events to the specified topic. /// diff --git a/src/Dapr.Client/InvocationHandler.cs b/src/Dapr.Client/InvocationHandler.cs index 1e9000c4d..1b55436aa 100644 --- a/src/Dapr.Client/InvocationHandler.cs +++ b/src/Dapr.Client/InvocationHandler.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -82,6 +82,12 @@ public string DaprEndpoint } } + /// + /// Gets or sets the default AppId used for service invocation + /// + /// The AppId used for service invocation + public string? DefaultAppId { get; set; } + // Internal for testing internal string? DaprApiToken { @@ -128,13 +134,23 @@ internal bool TryRewriteUri(Uri? uri, [NotNullWhen(true)] out Uri? rewritten) return false; } + string host; + + if (this.DefaultAppId is not null && uri.Host.Equals(this.DefaultAppId, StringComparison.InvariantCultureIgnoreCase)) + { + host = this.DefaultAppId; + } + else + { + host = uri.Host; + } var builder = new UriBuilder(uri) { Scheme = this.parsedEndpoint.Scheme, Host = this.parsedEndpoint.Host, Port = this.parsedEndpoint.Port, - Path = $"/v1.0/invoke/{uri.Host}/method" + uri.AbsolutePath, + Path = $"/v1.0/invoke/{host}/method" + uri.AbsolutePath, }; rewritten = builder.Uri; diff --git a/test/Dapr.Client.Test/DaprClientTest.cs b/test/Dapr.Client.Test/DaprClientTest.cs index a822bdf89..01d22edcf 100644 --- a/test/Dapr.Client.Test/DaprClientTest.cs +++ b/test/Dapr.Client.Test/DaprClientTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Client.Test/InvocationHandlerTests.cs b/test/Dapr.Client.Test/InvocationHandlerTests.cs index c6adb93df..3dac84113 100644 --- a/test/Dapr.Client.Test/InvocationHandlerTests.cs +++ b/test/Dapr.Client.Test/InvocationHandlerTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ public class InvocationHandlerTests public void DaprEndpoint_InvalidScheme() { var handler = new InvocationHandler(); - var ex = Assert.Throws(() => - { + var ex = Assert.Throws(() => + { handler.DaprEndpoint = "ftp://localhost:3500"; }); @@ -43,7 +43,7 @@ public void DaprEndpoint_InvalidUri() { var handler = new InvocationHandler(); Assert.Throws(() => - { + { handler.DaprEndpoint = ""; }); @@ -79,17 +79,50 @@ public void TryRewriteUri_FailsForRelativeUris() } [Theory] - [InlineData("http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] - [InlineData("http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] - [InlineData("http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] - [InlineData("http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] - public void TryRewriteUri_RewritesUriToDaprInvoke(string uri, string expected) + [InlineData(null, "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("bank", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://Bank", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://Bank:3939", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("app-id.with.dots", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("invalid", "http://app-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData(null, "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData("App-id.with.dots", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/App-id.with.dots/method/")] + [InlineData("invalid", "http://App-id.with.dots", "https://some.host:3499/v1.0/invoke/app-id.with.dots/method/")] + [InlineData(null, "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("bank", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("invalid", "http://bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData("Bank", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/Bank/method/")] + [InlineData("invalid", "http://Bank:3939/", "https://some.host:3499/v1.0/invoke/bank/method/")] + [InlineData(null, "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("bank", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("invalid", "http://bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData(null, "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData("Bank", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/Bank/method/some/path")] + [InlineData("invalid", "http://Bank:3939/some/path", "https://some.host:3499/v1.0/invoke/bank/method/some/path")] + [InlineData(null, "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("bank", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("invalid", "http://bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData(null, "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + [InlineData("Bank", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/Bank/method/some/path?q=test&p=another#fragment")] + [InlineData("invalid", "http://Bank:3939/some/path?q=test&p=another#fragment", "https://some.host:3499/v1.0/invoke/bank/method/some/path?q=test&p=another#fragment")] + public void TryRewriteUri_WithNoAppId_RewritesUriToDaprInvoke(string? appId, string uri, string expected) { var handler = new InvocationHandler() { DaprEndpoint = "https://some.host:3499", + DefaultAppId = appId, }; Assert.True(handler.TryRewriteUri(new Uri(uri), out var rewritten)); @@ -97,12 +130,12 @@ public void TryRewriteUri_RewritesUriToDaprInvoke(string uri, string expected) } [Fact] - public async Task SendAsync_InvalidUri_ThrowsException() + public async Task SendAsync_InvalidNotSetUri_ThrowsException() { var handler = new InvocationHandler(); var ex = await Assert.ThrowsAsync(async () => { - await CallSendAsync(handler, new HttpRequestMessage(){ }); // No URI set + await CallSendAsync(handler, new HttpRequestMessage() { }); // No URI set }); Assert.Contains("The request URI '' is not a valid Dapr service invocation destination.", ex.Message); @@ -132,6 +165,31 @@ public async Task SendAsync_RewritesUri() Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); } + [Fact] + public async Task SendAsync_RewritesUri_AndAppId() + { + var uri = "http://bank/accounts/17?"; + + var capture = new CaptureHandler(); + var handler = new InvocationHandler() + { + InnerHandler = capture, + + DaprEndpoint = "https://localhost:5000", + DaprApiToken = null, + DefaultAppId = "Bank" + }; + + var request = new HttpRequestMessage(HttpMethod.Post, uri); + var response = await CallSendAsync(handler, request); + + Assert.Equal("https://localhost:5000/v1.0/invoke/Bank/method/accounts/17?", capture.RequestUri?.OriginalString); + Assert.Null(capture.DaprApiToken); + + Assert.Equal(uri, request.RequestUri?.OriginalString); + Assert.False(request.Headers.TryGetValues("dapr-api-token", out _)); + } + [Fact] public async Task SendAsync_RewritesUri_AndAddsApiToken() { @@ -164,7 +222,7 @@ private async Task CallSendAsync(InvocationHandler handler, try { - return await (Task)method!.Invoke(handler, new object[]{ message, cancellationToken, })!; + return await (Task)method!.Invoke(handler, new object[] { message, cancellationToken, })!; } catch (TargetInvocationException tie) // reflection always adds an extra layer of exceptions. { From 034de3e233dbff08a3d45b15840c2f98bdeef4e9 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 16 Feb 2024 17:10:46 +0000 Subject: [PATCH 45/84] Enable vault name mapping and error suppression (#1231) * Added documentation detailing how serialization works using the DataContract serialization framework. (#1222) Signed-off-by: Whit Waldo Signed-off-by: James Croft * Weakly typed actor polymorphic and null responses (#1214) Signed-off-by: Remco Blok Co-authored-by: Remco Blok Co-authored-by: Phillip Hoff Signed-off-by: James Croft * Enable vault name mapping and error suppression Signed-off-by: Yash Nisar Signed-off-by: James Croft * Add additional secret descriptor constructor for required without key map Signed-off-by: James Croft * Update configuration load exception rethrow to match rules Signed-off-by: James Croft * Add tests for required/not required exception handling Signed-off-by: James Croft * Implementing Cryptography building block in .NET (#1217) * Added method to DaprClient and GRPC implementation to call cryptography proto endpoints Signed-off-by: Whit Waldo * First pass at implementing all exposed Cryptography methods on Go interface Signed-off-by: Whit Waldo * Added examples for Cryptography block Signed-off-by: Whit Waldo * Added missing copyright statements Signed-off-by: Whit Waldo * Updated to properly support Crypto API this time Signed-off-by: Whit Waldo * Added copyright statements Signed-off-by: Whit Waldo * Removed deprecated examples as the subtle APIs are presently disabled Signed-off-by: Whit Waldo * Updated example to reflect new API shape Signed-off-by: Whit Waldo * Updated example and readme Signed-off-by: Whit Waldo * Added overloads for encrypting/decrypting streams instead of just fixed byte arrays. Added example demonstrating the same encrypting a file via a FileStream and decrypting from a MemoryStream. Signed-off-by: Whit Waldo * Added some unit tests to pair with the implementation Signed-off-by: Whit Waldo * Added null check for the stream argument Signed-off-by: Whit Waldo * Changed case of the arguments as they should read "plaintext" and not "plainText" Signed-off-by: Whit Waldo * Reduced number of encryption implementations by just wrapping byte array into memory stream Signed-off-by: Whit Waldo * Constrainted returned member types per review suggestion Signed-off-by: Whit Waldo * Updated methods to use ReadOnlyMemory instead of byte[] - updated implementations to use low-allocation spans where possible (though ToArray is necessary to wrap with MemoryStream). Signed-off-by: Whit Waldo * Updated to use encryption/decryption options instead of lots of method overload variations. Simplified gRPC implementation to use fewer methods. Applied argument name updates applied previously (plainText -> plaintext). Signed-off-by: Whit Waldo * Updated tests Signed-off-by: Whit Waldo * Removed unused reference Signed-off-by: Whit Waldo * Updated examples to reflect new method shapes. Downgraded package to .net 6 instead of .net 8 per review suggestion. Signed-off-by: Whit Waldo * Updated to reflect non-aliased values per review suggestion Signed-off-by: Whit Waldo * Update to ensure that both send/receive streams run at the same time instead of sequentially. Signed-off-by: Whit Waldo * Updated to support streamed results in addition to fixed byte arrays. Refactored implementation to minimize duplicative code. Signed-off-by: Whit Waldo * Updated example to fix compile issue Signed-off-by: Whit Waldo * Removed encrypt/decrypt methods that accepted streams and returned ReadOnlyMemory. Marked implementations that use this on the gRPC class as private instead. Signed-off-by: Whit Waldo * Added missing Obsolete attributes on Encrypt/Decrypt methods. Added overloads on decrypt methods that do not require a DecryptionOptions to be passed in. Signed-off-by: Whit Waldo * Updated encrypt/decrypt options so the streaming block size no longer uses a uint. Added validation in its place to ensure the value provided is never less than or equal to 0. Signed-off-by: Whit Waldo * Updated how validation works in the options to accommodate lack of the shorter variation in .NET 6 Signed-off-by: Whit Waldo * Updated names of encrypt/decrypt streaming methods so everything uses just EncryptAsync or DecryptAsync Signed-off-by: Whit Waldo * Fixed regression that would have prevented data from being sent entirely to the sidecar. Also simplified operation per suggestion in review. Signed-off-by: Whit Waldo * Updated examples to reflect changed API Signed-off-by: Whit Waldo * Updated so IAsyncEnumerable methods (encrypt and decrypt) return IAsyncEnumerable> instead of IAsyncEnumerable. Signed-off-by: Whit Waldo * Updated example to reflect change from IAsyncEnumerable to IAsyncEnumerable> Signed-off-by: Whit Waldo * Avoiding allocation by using MemoryMarshal instead of .ToArray() to create MemoryStream from ReadOnlyMemory. Signed-off-by: Whit Waldo * Performance updates to minimize unnecessary byte array copies and eliminate unnecessary allocations. Signed-off-by: Whit Waldo * Removed unnecessary return from SendPlaintextStreamAsync and SendCiphertextStreamAsync methods Signed-off-by: Whit Waldo * Updated exception text to be more specific as to what's wrong with the input value. Signed-off-by: Whit Waldo * Minor tweak to prefer using using a Memory Signed-off-by: Whit Waldo * Deduplicated some of the Decrypt methods, simplifying the implementation Signed-off-by: Whit Waldo * Eliminated duplicate encryption method, simplifying implementation Signed-off-by: Whit Waldo * Updated to eliminate an unnecessary `await` and `async foreach`. Signed-off-by: Whit Waldo * Updated stream example to reflect the changes to the API shape Signed-off-by: Whit Waldo * Added notes about operations with stream-based data Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: James Croft * Update DaprSecretDescriptor constructors and documentation Signed-off-by: James Croft Signed-off-by: James Croft * Remove DaprSecretStoreConfigurationProvider Console.WriteLine Signed-off-by: James Croft --------- Signed-off-by: Whit Waldo Signed-off-by: James Croft Signed-off-by: Remco Blok Signed-off-by: Yash Nisar Signed-off-by: James Croft Co-authored-by: Whit Waldo Co-authored-by: Remco Blok Co-authored-by: Remco Blok Co-authored-by: Phillip Hoff Co-authored-by: Yash Nisar --- .../DaprSecretDescriptor.cs | 36 ++++- .../DaprSecretStoreConfigurationProvider.cs | 32 +++- .../Dapr.Extensions.Configuration.Test.csproj | 1 + ...aprSecretStoreConfigurationProviderTest.cs | 152 ++++++++++++++---- 4 files changed, 181 insertions(+), 40 deletions(-) diff --git a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs index e708ad712..6d86dc046 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretDescriptor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,8 +21,11 @@ namespace Dapr.Extensions.Configuration public class DaprSecretDescriptor { /// - /// Gets or sets the secret name. + /// The name of the secret to retrieve from the Dapr secret store. /// + /// + /// If the is not specified, this value will also be used as the key to retrieve the secret from the associated source secret store. + /// public string SecretName { get; } /// @@ -31,20 +34,39 @@ public class DaprSecretDescriptor public IReadOnlyDictionary Metadata { get; } /// - /// Secret Descriptor Construcutor + /// A value indicating whether to throw an exception if the secret is not found in the source secret store. + /// + /// + /// Setting this value to will suppress the exception; otherwise, will not. + /// + public bool IsRequired { get; } + + /// + /// The secret key that maps to the to retrieve from the source secret store. + /// + /// + /// Use this property when the does not match the key used to retrieve the secret from the source secret store. + /// + public string SecretKey { get; } + + /// + /// Secret Descriptor Constructor /// - public DaprSecretDescriptor(string secretName) : this(secretName, new Dictionary()) + public DaprSecretDescriptor(string secretName, bool isRequired = true, string secretKey = "") + : this(secretName, new Dictionary(), isRequired, secretKey) { } /// - /// Secret Descriptor Construcutor + /// Secret Descriptor Constructor /// - public DaprSecretDescriptor(string secretName, IReadOnlyDictionary metadata) + public DaprSecretDescriptor(string secretName, IReadOnlyDictionary metadata, bool isRequired = true, string secretKey = "") { SecretName = secretName; Metadata = metadata; + IsRequired = isRequired; + SecretKey = string.IsNullOrEmpty(secretKey) ? secretName : secretKey; } } -} \ No newline at end of file +} diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index da5349c30..5991a7dad 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ public DaprSecretStoreConfigurationProvider( bool normalizeKey, IEnumerable secretDescriptors, DaprClient client) : this(store, normalizeKey, null, secretDescriptors, client, DefaultSidecarWaitTimeout) - { + { } /// @@ -181,6 +181,10 @@ private string NormalizeKey(string key) return key; } + /// + /// Loads the configuration by calling the asynchronous LoadAsync method and blocking the calling + /// thread until the operation is completed. + /// public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult(); private async Task LoadAsync() @@ -197,16 +201,34 @@ private async Task LoadAsync() { foreach (var secretDescriptor in secretDescriptors) { - var result = await client.GetSecretAsync(store, secretDescriptor.SecretName, secretDescriptor.Metadata).ConfigureAwait(false); + + Dictionary result; + + try + { + result = await client + .GetSecretAsync(store, secretDescriptor.SecretKey, secretDescriptor.Metadata) + .ConfigureAwait(false); + } + catch (DaprException) + { + if (secretDescriptor.IsRequired) + { + throw; + } + result = new Dictionary(); + } foreach (var key in result.Keys) { if (data.ContainsKey(key)) { - throw new InvalidOperationException($"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); + throw new InvalidOperationException( + $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); } - data.Add(normalizeKey ? NormalizeKey(key) : key, result[key]); + data.Add(normalizeKey ? NormalizeKey(secretDescriptor.SecretName) : secretDescriptor.SecretName, + result[key]); } } diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 2e4523582..7d11d5c40 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -7,6 +7,7 @@ + all diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index 488c94983..d35275dd1 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ using FluentAssertions; using Grpc.Net.Client; using Microsoft.Extensions.Configuration; +using Moq; using Xunit; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; @@ -142,23 +143,23 @@ public void AddDaprSecretStore_UsingDescriptors_DuplicateSecret_ReportsError() [Fact] public void LoadSecrets_FromSecretStoreThatReturnsOneValue() { - // Configure Client - var httpClient = new TestHttpClient() + var storeName = "store"; + var secretKey = "secretName"; + var secretValue = "secret"; + + var secretDescriptors = new[] { - Handler = async (entry) => - { - var secrets = new Dictionary() { { "secretName", "secret" } }; - await SendResponseWithSecrets(secrets, entry); - } + new DaprSecretDescriptor(secretKey), }; - var daprClient = new DaprClientBuilder() - .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) - .Build(); + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) .Build(); config["secretName"].Should().Be("secret"); @@ -167,32 +168,127 @@ public void LoadSecrets_FromSecretStoreThatReturnsOneValue() [Fact] public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() { - // Configure Client - var httpClient = new TestHttpClient() + var storeName = "store"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; + + var secretDescriptors = new[] { - Handler = async (entry) => - { - var secrets = new Dictionary() { - { "first_secret", "secret1" }, - { "second_secret", "secret2" }}; - await SendResponseWithSecrets(secrets, entry); - } + new DaprSecretDescriptor(firstSecretKey), + new DaprSecretDescriptor(secondSecretKey), + }; + + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, firstSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue } }); + + daprClient.Setup(c => c.GetSecretAsync(storeName, secondSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secondSecretKey, secondSecretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[firstSecretKey].Should().Be(firstSecretValue); + config[secondSecretKey].Should().Be(secondSecretValue); + } + + [Fact] + public void LoadSecrets_FromSecretStoreWithADifferentSecretKeyAndName() + { + var storeName = "store"; + var secretKey = "Microsservice-DatabaseConnStr"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + var secretValue = "secret1"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), true, + secretKey) + }; + + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, secretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { secretKey, secretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[secretName].Should().Be(secretValue); + } + + [Fact] + public void LoadSecrets_FromSecretStoreNotRequiredAndDoesNotExist_ShouldNotThrowException() + { + var storeName = "store"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), false) + }; + + var httpClient = new TestHttpClient + { + Handler = async entry => + { + await SendEmptyResponse(entry); + } }; var daprClient = new DaprClientBuilder() .UseHttpClientFactory(() => httpClient) - .UseGrpcChannelOptions(new GrpcChannelOptions { HttpClient = httpClient }) .Build(); var config = CreateBuilder() - .AddDaprSecretStore("store", new DaprSecretDescriptor[] { new DaprSecretDescriptor("secretName") }, daprClient) + .AddDaprSecretStore(storeName, secretDescriptors, daprClient) + .Build(); + + config[secretName].Should().BeNull(); + } + + [Fact] + public void LoadSecrets_FromSecretStoreRequiredAndDoesNotExist_ShouldThrowException() + { + var storeName = "store"; + var secretName = "ConnectionStrings:DatabaseConnStr"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(secretName, new Dictionary(), true) + }; + + var httpClient = new TestHttpClient + { + Handler = async entry => + { + await SendEmptyResponse(entry); + } + }; + + var daprClient = new DaprClientBuilder() + .UseHttpClientFactory(() => httpClient) + .Build(); + + var ex = Assert.Throws(() => + { + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient) .Build(); + }); - config["first_secret"].Should().Be("secret1"); - config["second_secret"].Should().Be("secret2"); + Assert.Contains("Secret", ex.Message); } - //Here + [Fact] public void AddDaprSecretStore_WithoutStore_ReportsError() { @@ -565,7 +661,7 @@ await SendResponseWithSecrets(new Dictionary() ["otherSecretName≡value"] = "secret", }, entry); } - } + } } }; From 83858d779b656af2ecb76d3f6f7020e4fd264fb8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 16 Feb 2024 14:23:58 -0500 Subject: [PATCH 46/84] Add overload to deserialize GetBulkStateAsync item values (#1173) * Adds overload to BulkStateItem and GetBulkStateAsync to perform SDK-based deserialization of returned values instead of strictly returning serialized strings. Signed-off-by: Whit Waldo * Updated method summary to better direct user towards one method or the other (typed or not) Signed-off-by: Whit Waldo * Added comments to the typed BulkStateItem to better reflect the deserialized nature of the value. Signed-off-by: Whit Waldo * Refactored GetBulkStateAsync method to a shared private method so both the non-generic and generic public methods can deserialize the data once as necessary. Signed-off-by: Whit Waldo * Removed excessive space in comment. Signed-off-by: Whit Waldo * Formatting: If we're separating parameters to separate lines, convention requires each have their own line. Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Co-authored-by: halspang <70976921+halspang@users.noreply.github.com> Co-authored-by: Phillip Hoff --- src/Dapr.Client/BulkStateItem.cs | 38 ++++++++++++++++++ src/Dapr.Client/DaprClient.cs | 14 +++++++ src/Dapr.Client/DaprClientGrpc.cs | 55 ++++++++++++++++++++++++--- test/Dapr.Client.Test/StateApiTest.cs | 24 ++++++++++++ 4 files changed, 126 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Client/BulkStateItem.cs b/src/Dapr.Client/BulkStateItem.cs index fb717e1a8..5b30ddf21 100644 --- a/src/Dapr.Client/BulkStateItem.cs +++ b/src/Dapr.Client/BulkStateItem.cs @@ -49,4 +49,42 @@ public BulkStateItem(string key, string value, string etag) /// public string ETag { get; } } + + /// + /// Represents a state object returned from a bulk get state operation where the value has + /// been deserialized to the specified type. + /// + public readonly struct BulkStateItem + { + /// + /// Initializes a new instance of the class. + /// + /// The state key. + /// The typed value. + /// The ETag. + /// + /// Application code should not need to create instances of . + /// + public BulkStateItem(string key, TValue value, string etag) + { + this.Key = key; + this.Value = value; + this.ETag = etag; + } + + /// + /// Gets the state key. + /// + public string Key { get; } + + /// + /// Gets the deserialized value of the indicated type. + /// + public TValue Value { get; } + + /// + /// Get the ETag. + /// + public string ETag { get; } + } } diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 7f094804f..21777105b 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -717,6 +717,20 @@ public abstract Task InvokeMethodGrpcAsync( /// A that will return the list of values when the operation has completed. public abstract Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// + /// Gets a list of deserialized values associated with the from the Dapr state store. This overload should be used + /// if you expect the values of all the retrieved items to match the shape of the indicated . If you expect that + /// the values may differ in type from one another, do not specify the type parameter and instead use the original method + /// so the serialized string values will be returned instead. + /// + /// The name of state store to read from. + /// The list of keys to get values for. + /// The number of concurrent get operations the Dapr runtime will issue to the state store. a value equal to or smaller than 0 means max parallelism. + /// A collection of metadata key-value pairs that will be provided to the state store. The valid metadata keys and values are determined by the type of state store used. + /// A that can be used to cancel the operation. + /// A that will return the list of deserialized values when the operation has completed. + public abstract Task>> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default); + /// /// Saves a list of to the Dapr state store. /// diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 9c99b9eee..f856b87e6 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -601,7 +601,50 @@ public override async Task InvokeMethodGrpcAsync #region State Apis + /// public override async Task> GetBulkStateAsync(string storeName, IReadOnlyList keys, int? parallelism, IReadOnlyDictionary metadata = default, CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); + + var bulkResponse = new List(); + foreach (var item in rawBulkState) + { + bulkResponse.Add(new BulkStateItem(item.Key, item.Value.ToStringUtf8(), item.Etag)); + } + + return bulkResponse; + } + + /// + public override async Task>> GetBulkStateAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) + { + var rawBulkState = await GetBulkStateRawAsync(storeName, keys, parallelism, metadata, cancellationToken); + + var bulkResponse = new List>(); + foreach (var item in rawBulkState) + { + var deserializedValue = TypeConverters.FromJsonByteString(item.Value, this.JsonSerializerOptions); + bulkResponse.Add(new BulkStateItem(item.Key, deserializedValue, item.Etag)); + } + + return bulkResponse; + } + + /// + /// Retrieves the bulk state data, but rather than deserializing the values, leaves the specific handling + /// to the public callers of this method to avoid duplicate deserialization. + /// + private async Task> GetBulkStateRawAsync( + string storeName, + IReadOnlyList keys, + int? parallelism, + IReadOnlyDictionary metadata = default, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(storeName, nameof(storeName)); if (keys.Count == 0) @@ -609,7 +652,7 @@ public override async Task> GetBulkStateAsync(strin var envelope = new Autogenerated.GetBulkStateRequest() { - StoreName = storeName, + StoreName = storeName, Parallelism = parallelism ?? default }; @@ -632,18 +675,20 @@ public override async Task> GetBulkStateAsync(strin } catch (RpcException ex) { - throw new DaprException("State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + throw new DaprException( + "State operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); } - var bulkResponse = new List(); + var bulkResponse = new List<(string Key, string Etag, ByteString Value)>(); foreach (var item in response.Items) { - bulkResponse.Add(new BulkStateItem(item.Key, item.Data.ToStringUtf8(), item.Etag)); + bulkResponse.Add((item.Key, item.Etag, item.Data)); } return bulkResponse; } - + /// public override async Task GetStateAsync( string storeName, diff --git a/test/Dapr.Client.Test/StateApiTest.cs b/test/Dapr.Client.Test/StateApiTest.cs index cfa664663..2595fb006 100644 --- a/test/Dapr.Client.Test/StateApiTest.cs +++ b/test/Dapr.Client.Test/StateApiTest.cs @@ -75,6 +75,30 @@ public async Task GetBulkStateAsync_CanReadState() state.Should().HaveCount(1); } + [Fact] + public async Task GetBulkStateAsync_CanReadDeserializedState() + { + await using var client = TestClient.CreateForDaprClient(); + + var key = "test"; + var request = await client.CaptureGrpcRequestAsync(async daprClient => + { + return await daprClient.GetBulkStateAsync("testStore", new List() {key}, null); + }); + + // Create Response & Respond + const string size = "small"; + const string color = "yellow"; + var data = new Widget() {Size = size, Color = color}; + var envelope = MakeGetBulkStateResponse(key, data); + var state = await request.CompleteWithMessageAsync(envelope); + + // Get response and validate + state.Should().HaveCount(1); + state[0].Value.Size.Should().Match(size); + state[0].Value.Color.Should().Match(color); + } + [Fact] public async Task GetBulkStateAsync_WrapsRpcException() { From e244e88ac137a77aafde0029e52f309ddf58d2c2 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 16 Feb 2024 11:29:42 -0800 Subject: [PATCH 47/84] Use TryAddSingleton() for registering services. (#1238) Signed-off-by: Phillip Hoff --- .../DaprMvcBuilderExtensions.cs | 21 +++++++------------ .../DaprServiceCollectionExtensions.cs | 13 ------------ .../DaprMvcBuilderExtensionsTest.cs | 18 ++++++++++++++++ .../DaprServiceCollectionExtensionsTest.cs | 18 ++++++++++++++++ 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs index b775e61ae..6195b9c30 100644 --- a/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprMvcBuilderExtensions.cs @@ -21,6 +21,7 @@ namespace Microsoft.Extensions.DependencyInjection using Dapr.Client; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; + using Microsoft.Extensions.DependencyInjection.Extensions; /// /// Provides extension methods for . @@ -40,27 +41,19 @@ public static IMvcBuilder AddDapr(this IMvcBuilder builder, Action s.ImplementationType == typeof(DaprMvcMarkerService))) - { - return builder; - } - builder.Services.AddDaprClient(configureClient); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.Configure(options => { - options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); + if (!options.ModelBinderProviders.Any(p => p is StateEntryModelBinderProvider)) + { + options.ModelBinderProviders.Insert(0, new StateEntryModelBinderProvider()); + } }); return builder; } - - private class DaprMvcMarkerService - { - } } } diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 1da42243d..8491cb9b2 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -36,15 +36,6 @@ public static void AddDaprClient(this IServiceCollection services, Action s.ImplementationType == typeof(DaprClientMarkerService))) - { - return; - } - - services.AddSingleton(); - services.TryAddSingleton(_ => { var builder = new DaprClientBuilder(); @@ -56,9 +47,5 @@ public static void AddDaprClient(this IServiceCollection services, Action(); + + Assert.NotNull(daprClient); + } +#endif } } diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 6a581d228..614faf5e4 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -47,5 +47,23 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce() Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); } + +#if NET8_0_OR_GREATER + [Fact] + public void AddDaprClient_WithKeyedServices() + { + var services = new ServiceCollection(); + + services.AddKeyedSingleton("key1", new Object()); + + services.AddDaprClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var daprClient = serviceProvider.GetService(); + + Assert.NotNull(daprClient); + } +#endif } } From c07eb698ac5d1b152a60d76c64af4841ffa07397 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 16 Feb 2024 11:35:29 -0800 Subject: [PATCH 48/84] Source generated actor clients (#1165) Signed-off-by: Phillip Hoff --- .github/workflows/itests.yml | 20 +- all.sln | 44 ++ .../ActorClient/ActorClient.csproj | 22 + .../ActorClient/IClientActor.cs | 28 + .../GeneratedActor/ActorClient/Program.cs | 30 + .../ActorCommon/ActorCommon.csproj | 14 + .../ActorCommon/IRemoteActor.cs | 25 + .../ActorService/ActorService.csproj | 15 + .../GeneratedActor/ActorService/Program.cs | 36 + .../Properties/launchSettings.json | 31 + .../ActorService/RemoteActor.cs | 45 ++ .../ActorService/appsettings.Development.json | 8 + .../ActorService/appsettings.json | 9 + examples/GeneratedActor/README.md | 115 +++ .../ActorClientGenerator.cs | 303 ++++++++ .../Dapr.Actors.Generators.csproj | 45 ++ .../ActorClientGeneratorTests.cs | 696 ++++++++++++++++++ .../AdditionalMetadataReferences.cs | 21 + .../CSharpSourceGeneratorVerifier.cs | 81 ++ .../Dapr.Actors.Generators.Test.csproj | 32 + .../GlobalUsings.cs | 14 + .../ActorState.cs | 40 + .../ActorWebApplicationFactory.cs | 57 ++ .../Clients/GeneratedClientTests.cs | 107 +++ .../Clients/IClientActor.cs | 28 + .../Clients/IRemoteActor.cs | 23 + .../Clients/RemoteActor.cs | 50 ++ .../Dapr.E2E.Test.Actors.Generators.csproj | 32 + .../DaprSidecarFactory.cs | 151 ++++ .../GlobalUsings.cs | 14 + .../IPingActor.cs | 21 + .../PortManager.cs | 63 ++ .../XUnitLoggingProvider.cs | 76 ++ test/Dapr.E2E.Test.App/Actors/TimerActor.cs | 2 +- 34 files changed, 2295 insertions(+), 3 deletions(-) create mode 100644 examples/GeneratedActor/ActorClient/ActorClient.csproj create mode 100644 examples/GeneratedActor/ActorClient/IClientActor.cs create mode 100644 examples/GeneratedActor/ActorClient/Program.cs create mode 100644 examples/GeneratedActor/ActorCommon/ActorCommon.csproj create mode 100644 examples/GeneratedActor/ActorCommon/IRemoteActor.cs create mode 100644 examples/GeneratedActor/ActorService/ActorService.csproj create mode 100644 examples/GeneratedActor/ActorService/Program.cs create mode 100644 examples/GeneratedActor/ActorService/Properties/launchSettings.json create mode 100644 examples/GeneratedActor/ActorService/RemoteActor.cs create mode 100644 examples/GeneratedActor/ActorService/appsettings.Development.json create mode 100644 examples/GeneratedActor/ActorService/appsettings.json create mode 100644 examples/GeneratedActor/README.md create mode 100644 src/Dapr.Actors.Generators/ActorClientGenerator.cs create mode 100644 src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj create mode 100644 test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs create mode 100644 test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs create mode 100644 test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs create mode 100644 test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj create mode 100644 test/Dapr.Actors.Generators.Test/GlobalUsings.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/ActorState.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj create mode 100644 test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/PortManager.cs create mode 100644 test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 8a299bd59..3267783c0 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -113,7 +113,7 @@ jobs: - name: Build # disable deterministic builds, just for test run. Deterministic builds break coverage for some reason run: dotnet build --configuration release /p:GITHUB_ACTIONS=false - - name: Run Test + - name: Run General Tests id: tests continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. run: | @@ -128,8 +128,24 @@ jobs: /p:CollectCoverage=true \ /p:CoverletOutputFormat=opencover \ /p:GITHUB_ACTIONS=false + - name: Run Generators Tests + id: generator-tests + continue-on-error: true # proceed if tests fail, the report step will report the failure with more details. + run: | + dotnet test ${{ github.workspace }}/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj \ + --configuration Release \ + --framework ${{ matrix.framework }} \ + --no-build \ + --no-restore \ + --logger "trx;LogFilePrefix=${{ matrix.prefix }}" \ + --logger "GitHubActions;report-warnings=false" \ + --logger "console;verbosity=detailed" \ + --results-directory "${{ github.workspace }}/TestResults" \ + /p:CollectCoverage=true \ + /p:CoverletOutputFormat=opencover \ + /p:GITHUB_ACTIONS=false - name: Check test failure in PR - if: github.event_name == 'pull_request' && steps.tests.outcome != 'success' + if: github.event_name == 'pull_request' && (steps.tests.outcome != 'success' || steps.generator-tests.outcome != 'success') run: exit 1 - name: Upload test coverage uses: codecov/codecov-action@v1 diff --git a/all.sln b/all.sln index bf0b87352..be5620b64 100644 --- a/all.sln +++ b/all.sln @@ -104,6 +104,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "examples\Workflow\WorkflowUnitTest\WorkflowUnitTest.csproj", "{8CA09061-2BEF-4506-A763-07062D2BD6AC}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GeneratedActor", "GeneratedActor", "{7592AFA4-426B-42F3-AE82-957C86814482}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorClient", "examples\GeneratedActor\ActorClient\ActorClient.csproj", "{61C24126-F39D-4BEA-96DC-FC87BA730554}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorCommon", "examples\GeneratedActor\ActorCommon\ActorCommon.csproj", "{CB903D21-4869-42EF-BDD6-5B1CFF674337}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Generators", "src\Dapr.Actors.Generators\Dapr.Actors.Generators.csproj", "{980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorService", "examples\GeneratedActor\ActorService\ActorService.csproj", "{7C06FE2D-6C62-48F5-A505-F0D715C554DE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Generators.Test", "test\Dapr.Actors.Generators.Test\Dapr.Actors.Generators.Test.csproj", "{AF89083D-4715-42E6-93E9-38497D12A8A6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Actors.Generators", "test\Dapr.E2E.Test.Actors.Generators\Dapr.E2E.Test.Actors.Generators.csproj", "{B5CDB0DC-B26D-48F1-B934-FE5C1C991940}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject Global @@ -250,6 +263,30 @@ Global {DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.Build.0 = Release|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61C24126-F39D-4BEA-96DC-FC87BA730554}.Release|Any CPU.Build.0 = Release|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB903D21-4869-42EF-BDD6-5B1CFF674337}.Release|Any CPU.Build.0 = Release|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}.Release|Any CPU.Build.0 = Release|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C06FE2D-6C62-48F5-A505-F0D715C554DE}.Release|Any CPU.Build.0 = Release|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF89083D-4715-42E6-93E9-38497D12A8A6}.Release|Any CPU.Build.0 = Release|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940}.Release|Any CPU.Build.0 = Release|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -303,6 +340,13 @@ Global {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {7592AFA4-426B-42F3-AE82-957C86814482} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {61C24126-F39D-4BEA-96DC-FC87BA730554} = {7592AFA4-426B-42F3-AE82-957C86814482} + {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {7592AFA4-426B-42F3-AE82-957C86814482} + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {7592AFA4-426B-42F3-AE82-957C86814482} + {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {7592AFA4-426B-42F3-AE82-957C86814482} + {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/examples/GeneratedActor/ActorClient/ActorClient.csproj b/examples/GeneratedActor/ActorClient/ActorClient.csproj new file mode 100644 index 000000000..73b5c2027 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/ActorClient.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6 + 10.0 + enable + enable + + + + + + + + + + + + diff --git a/examples/GeneratedActor/ActorClient/IClientActor.cs b/examples/GeneratedActor/ActorClient/IClientActor.cs new file mode 100644 index 000000000..c5c732cb9 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/IClientActor.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Generators; + +namespace GeneratedActor; + +internal sealed record ClientState(string Value); + +[GenerateActorClient] +internal interface IClientActor +{ + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(ClientState state, CancellationToken cancellationToken = default); +} diff --git a/examples/GeneratedActor/ActorClient/Program.cs b/examples/GeneratedActor/ActorClient/Program.cs new file mode 100644 index 000000000..87f714907 --- /dev/null +++ b/examples/GeneratedActor/ActorClient/Program.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; +using GeneratedActor; + +Console.WriteLine("Testing generated client..."); + +var proxy = ActorProxy.Create(ActorId.CreateRandom(), "RemoteActor"); + +using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + +var client = new ClientActorClient(proxy); + +var state = await client.GetStateAsync(cancellationTokenSource.Token); + +await client.SetStateAsync(new ClientState("Hello, World!"), cancellationTokenSource.Token); + +Console.WriteLine("Done!"); diff --git a/examples/GeneratedActor/ActorCommon/ActorCommon.csproj b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj new file mode 100644 index 000000000..2cbc61e2c --- /dev/null +++ b/examples/GeneratedActor/ActorCommon/ActorCommon.csproj @@ -0,0 +1,14 @@ + + + + net6 + 10.0 + enable + enable + + + + + + + diff --git a/examples/GeneratedActor/ActorCommon/IRemoteActor.cs b/examples/GeneratedActor/ActorCommon/IRemoteActor.cs new file mode 100644 index 000000000..6d136a704 --- /dev/null +++ b/examples/GeneratedActor/ActorCommon/IRemoteActor.cs @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; + +namespace GeneratedActor; + +public sealed record RemoteState(string Value); + +public interface IRemoteActor : IActor +{ + Task GetState(); + + Task SetState(RemoteState state); +} diff --git a/examples/GeneratedActor/ActorService/ActorService.csproj b/examples/GeneratedActor/ActorService/ActorService.csproj new file mode 100644 index 000000000..a74104363 --- /dev/null +++ b/examples/GeneratedActor/ActorService/ActorService.csproj @@ -0,0 +1,15 @@ + + + + net6 + 10.0 + enable + enable + + + + + + + + diff --git a/examples/GeneratedActor/ActorService/Program.cs b/examples/GeneratedActor/ActorService/Program.cs new file mode 100644 index 000000000..f6e62f720 --- /dev/null +++ b/examples/GeneratedActor/ActorService/Program.cs @@ -0,0 +1,36 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using GeneratedActor; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddActors( + options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }); + +var app = builder.Build(); + +app.UseRouting(); + +#pragma warning disable ASP0014 +app.UseEndpoints( + endpoints => + { + endpoints.MapActorsHandlers(); + }); + +app.Run(); diff --git a/examples/GeneratedActor/ActorService/Properties/launchSettings.json b/examples/GeneratedActor/ActorService/Properties/launchSettings.json new file mode 100644 index 000000000..8fbb1f581 --- /dev/null +++ b/examples/GeneratedActor/ActorService/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56372", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5226", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/GeneratedActor/ActorService/RemoteActor.cs b/examples/GeneratedActor/ActorService/RemoteActor.cs new file mode 100644 index 000000000..f04921f69 --- /dev/null +++ b/examples/GeneratedActor/ActorService/RemoteActor.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace GeneratedActor; + +internal sealed class RemoteActor : Actor, IRemoteActor +{ + private readonly ILogger logger; + + private RemoteState currentState = new("default"); + + public RemoteActor(ActorHost host, ILogger logger) + : base(host) + { + this.logger = logger; + } + + public Task GetState() + { + this.logger.LogInformation("GetStateAsync called."); + + return Task.FromResult(this.currentState); + } + + public Task SetState(RemoteState state) + { + this.logger.LogInformation("SetStateAsync called."); + + this.currentState = state; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/examples/GeneratedActor/ActorService/appsettings.Development.json b/examples/GeneratedActor/ActorService/appsettings.Development.json new file mode 100644 index 000000000..ff66ba6b2 --- /dev/null +++ b/examples/GeneratedActor/ActorService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/GeneratedActor/ActorService/appsettings.json b/examples/GeneratedActor/ActorService/appsettings.json new file mode 100644 index 000000000..4d566948d --- /dev/null +++ b/examples/GeneratedActor/ActorService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/GeneratedActor/README.md b/examples/GeneratedActor/README.md new file mode 100644 index 000000000..cd595b30e --- /dev/null +++ b/examples/GeneratedActor/README.md @@ -0,0 +1,115 @@ +# Generated Actor Client Example + +An example of generating a strongly-typed actor client. + +## Prerequisites + +- [.NET 6+](https://dotnet.microsoft.com/download) installed +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) +- [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) + +## Overview + +Two options for invoking actor methods exist in the Dapr .NET SDK, a strongly-type (remoting) option and a loosely-typed (non-remoting) option. Each has its own advantages and disadvantages. A "middle" option also exists that combines the two and gains benefits of both without some of the disadvantages of either. Using .NET Source Generators, the Dapr .NET SDK can generate a strongly-typed client implementation that uses loosely-typed method invocation under the covers. + +Strongly-typed clients are generated by: + +1. Referencing the `Dapr.Actors.Generators` NuGet package. + + ```xml + + + + + + ``` + +1. Add the `Dapr.Actors.Generators.GenerateActorClientAttribute` to the actor interface. + + ```csharp + using Dapr.Actors.Generators; + + namespace Sample; + + internal sealed record SampleState(string Value); + + [GenerateActorClient] + internal interface ISampleActor + { + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(SampleState state, CancellationToken cancellationToken = default); + } + ``` + + > The `Dapr.Actors.Generators.ActorMethodAttribute` can be used to map interface methods definitions to specific actor methods should the names differ (e.g. the interface uses "Async" suffix common in .NET but the actor methods do not). + +1. A strongly-typed client will be generated that can be used to invoke actor methods. + + ```csharp + using Dapr.Actors; + using Dapr.Actors.Client; + using Sample; + + var proxy = ActorProxy.Create(ActorId.CreateRandom(), "SampleActor"); + + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + var client = new SampleActorClient(proxy); + + var state = await client.GetStateAsync(cancellationTokenSource.Token); + + await client.SetStateAsync(new SampleState("Hello, World!"), cancellationTokenSource.Token); + + ``` + +## Run the example + +### Start the ActorService + +Change directory to the `ActorService` folder: + +```bash +cd examples/GeneratedActor/ActorService +``` + +To start the `ActorService`, execute the following command: + +```bash +dapr run --app-id generated-service --app-port 5226 -- dotnet run +``` + +### Run the ActorClient + +Change directory to the `ActorClient` folder: + +```bash +cd examples/GeneratedActor/ActorClient +``` + +To run the `ActorClient`, execute the following command: + +```bash +dapr run --app-id generated-client -- dotnet run +``` + +### Expected output + +You should see the following output from the `ActorClient`: + +``` +== APP == Testing generated client... +== APP == Done! +``` + +You should see also see the following output from the `ActorService`: + +``` +== APP == info: GeneratedActor.RemoteActor[0] +== APP == GetStateAsync called. +== APP == info: GeneratedActor.RemoteActor[0] +== APP == SetStateAsync called. +``` \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs new file mode 100644 index 000000000..349d80188 --- /dev/null +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -0,0 +1,303 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Generators; + +/// +/// Generates strongly-typed actor clients that use the non-remoting actor proxy. +/// +[Generator] +public sealed class ActorClientGenerator : ISourceGenerator +{ + private const string GeneratorsNamespace = "Dapr.Actors.Generators"; + + private const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; + private const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; + + private const string GenerateActorClientAttribute = "GenerateActorClientAttribute"; + private const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttribute; + + private const string ActorMethodAttributeText = $@" + // + + #nullable enable + + using System; + + namespace {GeneratorsNamespace} + {{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class ActorMethodAttribute : Attribute + {{ + public string? Name {{ get; set; }} + }} + }}"; + + private const string GenerateActorClientAttributeText = $@" + // + + #nullable enable + + using System; + + namespace {GeneratorsNamespace} + {{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class GenerateActorClientAttribute : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} + }}"; + + private sealed class ActorInterfaceSyntaxReceiver : ISyntaxContextReceiver + { + private readonly List models = new(); + + public IEnumerable Models => this.models; + + #region ISyntaxContextReceiver Members + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is not InterfaceDeclarationSyntax interfaceDeclarationSyntax + || interfaceDeclarationSyntax.AttributeLists.Count == 0) + { + return; + } + + var interfaceSymbol = context.SemanticModel.GetDeclaredSymbol(interfaceDeclarationSyntax) as INamedTypeSymbol; + + if (interfaceSymbol is null + || !interfaceSymbol.GetAttributes().Any(a => a.AttributeClass?.ToString() == GenerateActorClientAttributeFullTypeName)) + { + return; + } + + this.models.Add(interfaceSymbol); + } + + #endregion + } + + #region ISourceGenerator Members + + /// + public void Execute(GeneratorExecutionContext context) + { + if (context.SyntaxContextReceiver is not ActorInterfaceSyntaxReceiver actorInterfaceSyntaxReceiver) + { + return; + } + + var actorMethodAttributeSymbol = context.Compilation.GetTypeByMetadataName(ActorMethodAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find ActorMethodAttribute."); + var generateActorClientAttributeSymbol = context.Compilation.GetTypeByMetadataName(GenerateActorClientAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find GenerateActorClientAttribute."); + var cancellationTokenSymbol = context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken") ?? throw new InvalidOperationException("Could not find CancellationToken."); + + foreach (var interfaceSymbol in actorInterfaceSyntaxReceiver.Models) + { + try + { + var actorInterfaceTypeName = interfaceSymbol.Name; + var fullyQualifiedActorInterfaceTypeName = interfaceSymbol.ToString(); + + var attributeData = interfaceSymbol.GetAttributes().Single(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); + + var accessibility = GetClientAccessibility(interfaceSymbol); + var clientTypeName = GetClientName(interfaceSymbol, attributeData); + var namespaceName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Namespace").Value.Value?.ToString() ?? interfaceSymbol.ContainingNamespace.ToDisplayString(); + + var members = interfaceSymbol.GetMembers().OfType().Where(m => m.MethodKind == MethodKind.Ordinary).ToList(); + + var methodImplementations = String.Join("\n", members.Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol))); + + var source = $@" +// + +namespace {namespaceName} +{{ + {accessibility} sealed class {clientTypeName} : {fullyQualifiedActorInterfaceTypeName} + {{ + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public {clientTypeName}(Dapr.Actors.Client.ActorProxy actorProxy) + {{ + this.actorProxy = actorProxy; + }} + + {methodImplementations} + }} +}} +"; + // Add the source code to the compilation + context.AddSource($"{namespaceName}.{clientTypeName}.g.cs", source); + } + catch (DiagnosticsException e) + { + foreach (var diagnostic in e.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + } + } + } + + /// + public void Initialize(GeneratorInitializationContext context) + { + /* + while (!Debugger.IsAttached) + { + System.Threading.Thread.Sleep(500); + } + */ + + context.RegisterForPostInitialization( + i => + { + i.AddSource($"{ActorMethodAttributeFullTypeName}.g.cs", ActorMethodAttributeText); + i.AddSource($"{GenerateActorClientAttributeFullTypeName}.g.cs", GenerateActorClientAttributeText); + }); + + context.RegisterForSyntaxNotifications(() => new ActorInterfaceSyntaxReceiver()); + } + + #endregion + + private static string GetClientAccessibility(INamedTypeSymbol interfaceSymbol) + { + return interfaceSymbol.DeclaredAccessibility switch + { + Accessibility.Public => "public", + Accessibility.Internal => "internal", + Accessibility.Private => "private", + Accessibility.Protected => "protected", + Accessibility.ProtectedAndInternal => "protected internal", + _ => throw new InvalidOperationException("Unexpected accessibility.") + }; + } + + private static string GetClientName(INamedTypeSymbol interfaceSymbol, AttributeData attributeData) + { + string? clientName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString(); + + clientName ??= $"{(interfaceSymbol.Name.StartsWith("I") ? interfaceSymbol.Name.Substring(1) : interfaceSymbol.Name)}Client"; + + return clientName; + } + + private static string GenerateMethodImplementation(IMethodSymbol method, INamedTypeSymbol generateActorClientAttributeSymbol, INamedTypeSymbol cancellationTokenSymbol) + { + int cancellationTokenIndex = method.Parameters.IndexOf(p => p.Type.Equals(cancellationTokenSymbol, SymbolEqualityComparer.Default)); + var cancellationTokenParameter = cancellationTokenIndex != -1 ? method.Parameters[cancellationTokenIndex] : null; + + if (cancellationTokenParameter is not null && cancellationTokenIndex != method.Parameters.Length - 1) + { + throw new DiagnosticsException(new[] + { + Diagnostic.Create( + new DiagnosticDescriptor( + "DAPR0001", + "Invalid method signature.", + "Cancellation tokens must be the last argument.", + "Dapr.Actors.Generators", + DiagnosticSeverity.Error, + true), + cancellationTokenParameter.Locations.First()) + }); + } + + if ((method.Parameters.Length > 1 && cancellationTokenIndex == -1) + || (method.Parameters.Length > 2)) + { + throw new DiagnosticsException(new[] + { + Diagnostic.Create( + new DiagnosticDescriptor( + "DAPR0002", + "Invalid method signature.", + "Only methods with a single argument or a single argument followed by a cancellation token are supported.", + "Dapr.Actors.Generators", + DiagnosticSeverity.Error, + true), + method.Locations.First()) + }); + } + + var attributeData = method.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); + + string? actualMethodName = attributeData?.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() ?? method.Name; + + var requestParameter = method.Parameters.Length > 0 && cancellationTokenIndex != 0 ? method.Parameters[0] : null; + + var returnTypeArgument = (method.ReturnType as INamedTypeSymbol)?.TypeArguments.FirstOrDefault(); + + string argumentDefinitions = String.Join(", ", method.Parameters.Select(p => $"{p.Type} {p.Name}")); + + if (cancellationTokenParameter is not null + && cancellationTokenParameter.IsOptional + && cancellationTokenParameter.HasExplicitDefaultValue + && cancellationTokenParameter.ExplicitDefaultValue is null) + { + argumentDefinitions = argumentDefinitions + " = default"; + } + + string argumentList = String.Join(", ", new[] { $@"""{actualMethodName}""" }.Concat(method.Parameters.Select(p => p.Name))); + + string templateArgs = + returnTypeArgument is not null + ? $"<{(requestParameter is not null ? $"{requestParameter.Type}, " : "")}{returnTypeArgument}>" + : ""; + + return + $@"public {method.ReturnType} {method.Name}({argumentDefinitions}) + {{ + return this.actorProxy.InvokeMethodAsync{templateArgs}({argumentList}); + }}"; + } +} + +internal static class Extensions +{ + public static int IndexOf(this IEnumerable source, Func predicate) + { + int index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } +} + +internal sealed class DiagnosticsException : Exception +{ + public DiagnosticsException(IEnumerable diagnostics) + : base(String.Join("\n", diagnostics.Select(d => d.ToString()))) + { + this.Diagnostics = diagnostics.ToArray(); + } + + public IEnumerable Diagnostics { get; } +} diff --git a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj new file mode 100644 index 000000000..a69f2d1a0 --- /dev/null +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -0,0 +1,45 @@ + + + + enable + enable + + + + true + + + + + + + + + + + + netstandard2.0 + + + + false + + + true + + + false + + + This package contains source generators for interacting with Actor services using Dapr. + $(PackageTags);Actors + + + + + + + + diff --git a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs new file mode 100644 index 000000000..ce4c0accd --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -0,0 +1,696 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Actors.Generators; + +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using VerifyCS = CSharpSourceGeneratorVerifier; + +public sealed class ActorClientGeneratorTests +{ + private const string ActorMethodAttributeText = $@" + // + + #nullable enable + + using System; + + namespace Dapr.Actors.Generators + {{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class ActorMethodAttribute : Attribute + {{ + public string? Name {{ get; set; }} + }} + }}"; + + private static readonly (string, SourceText) ActorMethodAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.ActorMethodAttribute.g.cs", SourceText.From(ActorMethodAttributeText, Encoding.UTF8)); + + private const string GenerateActorClientAttributeText = $@" + // + + #nullable enable + + using System; + + namespace Dapr.Actors.Generators + {{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class GenerateActorClientAttribute : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} + }}"; + + private static readonly (string, SourceText) GenerateActorClientAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs", SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8)); + + private static VerifyCS.Test CreateTest(string originalSource, string? generatedName = null, string? generatedSource = null) + { + var test = new VerifyCS.Test + { + TestState = + { + AdditionalReferences = { AdditionalMetadataReferences.Actors }, + Sources = { originalSource }, + GeneratedSources = + { + ActorMethodAttributeSource, + GenerateActorClientAttributeSource + }, + } + }; + + if (generatedName is not null && generatedSource is not null) + { + test.TestState.GeneratedSources.Add(($"Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/{generatedName}", SourceText.From(generatedSource, Encoding.UTF8))); + } + + return test; + } + + [Fact] + public async Task TestMethodWithNoArgumentsOrReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestInternalInterface() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestRenamedClient() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Name = ""MyTestActorClient"")] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + internal sealed class MyTestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestCustomNamespace() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient(Namespace = ""MyTest"")] + internal interface ITestActor + { + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace MyTest +{ + internal sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""TestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestRenamedMethod() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + [ActorMethod(Name = ""MyTestMethod"")] + Task TestMethod(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod() + { + return this.actorProxy.InvokeMethodAsync(""MyTestMethod""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithArgumentsButNoReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethod(TestValue value); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethod(Test.TestValue value) + { + return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithNoArgumentsButReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync() + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync""); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithArgumentsAndReturnValue() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading.Tasks; + +namespace Test +{ + public record TestRequestValue(int Value); + + public record TestReturnValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestRequestValue value); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestRequestValue value) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithCancellationTokenArgument() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithDefaultCancellationTokenArgument() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken = default); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken = default) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithValueAndCancellationTokenArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestValue value, CancellationToken cancellationToken); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithValueAndDefaultCancellationTokenArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(TestValue value, CancellationToken cancellationToken = default); + } +} +"; + + var generatedSource = @" +// + +namespace Test +{ + public sealed class TestActorClient : Test.ITestActor + { + private readonly Dapr.Actors.Client.ActorProxy actorProxy; + + public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) + { + this.actorProxy = actorProxy; + } + + public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken = default) + { + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + } + } +} +"; + + await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); + } + + [Fact] + public async Task TestMethodWithReversedArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(CancellationToken cancellationToken, int value); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0001", DiagnosticSeverity.Error) + .WithSpan(13, 48, 13, 65) + .WithMessage("Cancellation tokens must be the last argument.")); + + await test.RunAsync(); + } + + [Fact] + public async Task TestMethodWithTooManyArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(int value1, int value2); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) + .WithSpan(13, 14, 13, 29) + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + + await test.RunAsync(); + } + + [Fact] + public async Task TestMethodWithFarTooManyArguments() + { + var originalSource = @" +using Dapr.Actors.Generators; +using System.Threading; +using System.Threading.Tasks; + +namespace Test +{ + public record TestValue(int Value); + + [GenerateActorClient] + public interface ITestActor + { + Task TestMethodAsync(int value1, int value2, CancellationToken cancellationToken); + } +} +"; + + var test = CreateTest(originalSource); + + test.TestState.ExpectedDiagnostics.Add( + new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) + .WithSpan(13, 14, 13, 29) + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + + await test.RunAsync(); + } +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs b/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs new file mode 100644 index 000000000..afa557026 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/AdditionalMetadataReferences.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators; + +internal static class AdditionalMetadataReferences +{ + public static readonly MetadataReference Actors = MetadataReference.CreateFromFile(typeof(Dapr.Actors.Client.ActorProxy).Assembly.Location); +} \ No newline at end of file diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 000000000..435488c2c --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -0,0 +1,81 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; + +/// +/// From Roslyn Source Generators Cookbook: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators +/// +internal static class CSharpSourceGeneratorVerifier + where TSourceGenerator : ISourceGenerator, new() +{ + public class Test : CSharpSourceGeneratorTest + { + public Test() + { + int frameworkVersion = + #if NET6_0 + 6; + #elif NET7_0 + 7; + #elif NET8_0 + 8; + #endif + + // + // NOTE: Ordinarily we'd use the following: + // + // this.ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net60; + // + // However, Net70 and Net80 are not yet available in the current version of the Roslyn SDK. + // + + this.ReferenceAssemblies = + new ReferenceAssemblies( + $"net{frameworkVersion}.0", + new PackageIdentity( + "Microsoft.NETCore.App.Ref", + $"{frameworkVersion}.0.0"), + Path.Combine("ref", $"net{frameworkVersion}.0")); + } + + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + + return compilationOptions + .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + } + + public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + return nullableWarnings; + } + + protected override ParseOptions CreateParseOptions() + { + return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); + } + } +} \ 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 new file mode 100644 index 000000000..212faed2d --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -0,0 +1,32 @@ + + + + enable + enable + + true + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/test/Dapr.Actors.Generators.Test/GlobalUsings.cs b/test/Dapr.Actors.Generators.Test/GlobalUsings.cs new file mode 100644 index 000000000..48f0c59b2 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/GlobalUsings.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +global using Xunit; \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs b/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs new file mode 100644 index 000000000..6965c751c --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/ActorState.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal static class ActorState +{ + public static async Task EnsureReadyAsync(ActorId actorId, string actorType, ActorProxyOptions? options = null, CancellationToken cancellationToken = default) + where TActor : IPingActor + { + var pingProxy = ActorProxy.Create(actorId, actorType, options); + + while (true) + { + try + { + await pingProxy.Ping(); + + break; + } + catch (DaprApiException) + { + await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken); + } + } + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs b/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs new file mode 100644 index 000000000..b5e81b8aa --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/ActorWebApplicationFactory.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed record ActorWebApplicationOptions(Action ConfigureActors) +{ + public ILoggerProvider? LoggerProvider { get; init; } + + public string? Url { get; init; } +} + +internal sealed class ActorWebApplicationFactory +{ + public static WebApplication Create(ActorWebApplicationOptions options) + { + var builder = WebApplication.CreateBuilder(); + + if (options.LoggerProvider is not null) + { + builder.Logging.ClearProviders(); + builder.Logging.AddProvider(options.LoggerProvider); + } + + builder.Services.AddActors(options.ConfigureActors); + + var app = builder.Build(); + + if (options.Url is not null) + { + app.Urls.Add(options.Url); + } + + app.UseRouting(); + + #pragma warning disable ASP0014 + app.UseEndpoints( + endpoints => + { + endpoints.MapActorsHandlers(); + }); + + return app; + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs new file mode 100644 index 000000000..6079b5df7 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/GeneratedClientTests.cs @@ -0,0 +1,107 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; +using Dapr.Actors.Client; +using Xunit.Abstractions; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +public class GeneratedClientTests +{ + private readonly ILoggerProvider testLoggerProvider; + + public GeneratedClientTests(ITestOutputHelper testOutputHelper) + { + this.testLoggerProvider = new XUnitLoggingProvider(testOutputHelper); + } + + [Fact] + public async Task TestGeneratedClientAsync() + { + var portManager = new PortManager(); + + (int appPort, int clientAppHttpPort) = portManager.ReservePorts(); + + var templateSidecarOptions = new DaprSidecarOptions("template-app") + { + LoggerFactory = new LoggerFactory(new[] { this.testLoggerProvider }), + LogLevel = "debug" + }; + + var serviceAppSidecarOptions = templateSidecarOptions with + { + AppId = "service-app", + AppPort = appPort + }; + + var clientAppSidecarOptions = templateSidecarOptions with + { + AppId = "client-app", + DaprHttpPort = clientAppHttpPort + }; + + await using var app = ActorWebApplicationFactory.Create( + new ActorWebApplicationOptions(options => + { + options.UseJsonSerialization = true; + options.Actors.RegisterActor(); + }) + { + LoggerProvider = this.testLoggerProvider, + Url = $"http://localhost:{appPort}" + }); + + using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + // + // Start application... + // + + await app.StartAsync(cancellationTokenSource.Token); + + // + // Start sidecars... + // + + await using var serviceAppSidecar = DaprSidecarFactory.Create(serviceAppSidecarOptions); + + await serviceAppSidecar.StartAsync(cancellationTokenSource.Token); + + await using var clientAppSidecar = DaprSidecarFactory.Create(clientAppSidecarOptions); + + await clientAppSidecar.StartAsync(cancellationTokenSource.Token); + + // + // Ensure actor is ready... + // + + var actorId = ActorId.CreateRandom(); + var actorType = "RemoteActor"; + var actorOptions = new ActorProxyOptions { HttpEndpoint = $"http://localhost:{clientAppHttpPort}" }; + + await ActorState.EnsureReadyAsync(actorId, actorType, actorOptions, cancellationTokenSource.Token); + + // + // Start test... + // + + var actorProxy = ActorProxy.Create(actorId, actorType, actorOptions); + + var client = new ClientActorClient(actorProxy); + + var result = await client.GetStateAsync(cancellationTokenSource.Token); + + await client.SetStateAsync(new ClientState("updated state"), cancellationTokenSource.Token); + } +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs new file mode 100644 index 000000000..a6cf30a76 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/IClientActor.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Generators; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +internal record ClientState(string Value); + +[GenerateActorClient] +internal interface IClientActor +{ + [ActorMethod(Name = "GetState")] + Task GetStateAsync(CancellationToken cancellationToken = default); + + [ActorMethod(Name = "SetState")] + Task SetStateAsync(ClientState state, CancellationToken cancellationToken = default); +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs new file mode 100644 index 000000000..77ad6e75b --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/IRemoteActor.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +public record RemoteState(string Value); + +public interface IRemoteActor : IPingActor +{ + Task GetState(); + + Task SetState(RemoteState state); +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs b/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs new file mode 100644 index 000000000..9c049019d --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Clients/RemoteActor.cs @@ -0,0 +1,50 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors.Runtime; + +namespace Dapr.E2E.Test.Actors.Generators.Clients; + +internal sealed class RemoteActor : Actor, IRemoteActor +{ + private readonly ILogger logger; + + private RemoteState currentState = new("default"); + + public RemoteActor(ActorHost host, ILogger logger) + : base(host) + { + this.logger = logger; + } + + public Task GetState() + { + this.logger.LogInformation("GetStateAsync called."); + + return Task.FromResult(this.currentState); + } + + public Task SetState(RemoteState state) + { + this.logger.LogInformation("SetStateAsync called."); + + this.currentState = state; + + return Task.CompletedTask; + } + + public Task Ping() + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj new file mode 100644 index 000000000..8618647cb --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/Dapr.E2E.Test.Actors.Generators.csproj @@ -0,0 +1,32 @@ + + + + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs b/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs new file mode 100644 index 000000000..56d1954d4 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/DaprSidecarFactory.cs @@ -0,0 +1,151 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Diagnostics; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed record DaprSidecarOptions(string AppId) +{ + public int? AppPort { get; init; } + + public int? DaprGrpcPort { get; init;} + + public int? DaprHttpPort { get; init; } + + public ILoggerFactory? LoggerFactory { get; init; } + + public string? LogLevel { get; init; } +} + +internal sealed class DaprSidecar : IAsyncDisposable +{ + private const string StartupOutputString = "You're up and running! Dapr logs will appear here."; + + private readonly string appId; + private readonly Process process; + private readonly ILogger? logger; + private readonly TaskCompletionSource tcs = new(); + + public DaprSidecar(DaprSidecarOptions options) + { + string arguments = $"run --app-id {options.AppId}"; + + if (options.AppPort is not null) + { + arguments += $" --app-port {options.AppPort}"; + } + + if (options.DaprGrpcPort is not null) + { + arguments += $" --dapr-grpc-port {options.DaprGrpcPort}"; + } + + if (options.DaprHttpPort is not null) + { + arguments += $" --dapr-http-port {options.DaprHttpPort}"; + } + + if (options.LogLevel is not null) + { + arguments += $" --log-level {options.LogLevel}"; + } + + this.process = new Process + { + EnableRaisingEvents = false, // ? + StartInfo = + { + Arguments = arguments, + CreateNoWindow = true, + FileName = "dapr", + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + + if (options.LoggerFactory is not null) + { + this.logger = options.LoggerFactory.CreateLogger(options.AppId); + } + + this.process.OutputDataReceived += (_, args) => + { + if (args.Data is not null) + { + if (args.Data.Contains(StartupOutputString)) + { + this.tcs.SetResult(true); + } + + this.logger?.LogInformation(args.Data); + } + }; + + this.process.ErrorDataReceived += (_, args) => + { + if (args.Data is not null) + { + this.logger?.LogError(args.Data); + } + }; + + this.appId = options.AppId; + } + + public Task StartAsync(CancellationToken cancellationToken = default) + { + process.Start(); + + process.BeginErrorReadLine(); + process.BeginOutputReadLine(); + + return this.tcs.Task; + } + + public async Task StopAsync(CancellationToken cancellationToken = default) + { + var stopProcess = new Process + { + StartInfo = + { + Arguments = $"stop --app-id {this.appId}", + CreateNoWindow = true, + FileName = "dapr", + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden + } + }; + + stopProcess.Start(); + + await stopProcess.WaitForExitAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + } + + public async ValueTask DisposeAsync() + { + await StopAsync(CancellationToken.None); + } +} + +internal sealed class DaprSidecarFactory +{ + public static DaprSidecar Create(DaprSidecarOptions options) + { + return new(options); + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs b/test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs new file mode 100644 index 000000000..48f0c59b2 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/GlobalUsings.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +global using Xunit; \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs b/test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs new file mode 100644 index 000000000..484c4d150 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/IPingActor.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Actors; + +namespace Dapr.E2E.Test.Actors.Generators; + +public interface IPingActor : IActor +{ + Task Ping(); +} diff --git a/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs b/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs new file mode 100644 index 000000000..fcd296977 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/PortManager.cs @@ -0,0 +1,63 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Net.NetworkInformation; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed class PortManager +{ + private readonly ISet reservedPorts = new HashSet(); + + private readonly object reservationLock = new(); + + public int ReservePort(int rangeStart = 55000) + { + var ports = this.ReservePorts(1, rangeStart); + + return ports.First(); + } + + public (int, int) ReservePorts(int rangeStart = 55000) + { + var ports = this.ReservePorts(2, rangeStart).ToArray(); + + return (ports[0], ports[1]); + } + + public ISet ReservePorts(int count, int rangeStart = 55000) + { + lock (this.reservationLock) + { + var globalProperties = IPGlobalProperties.GetIPGlobalProperties(); + + var activePorts = + globalProperties + .GetActiveTcpListeners() + .Select(endPoint => endPoint.Port) + .ToHashSet(); + + var availablePorts = + Enumerable + .Range(rangeStart, Int32.MaxValue - rangeStart + 1) + .Where(port => !activePorts.Contains(port)) + .Where(port => !this.reservedPorts.Contains(port)); + + var newReservedPorts = availablePorts.Take(count).ToHashSet(); + + this.reservedPorts.UnionWith(newReservedPorts); + + return newReservedPorts; + } + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs b/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs new file mode 100644 index 000000000..641d66d80 --- /dev/null +++ b/test/Dapr.E2E.Test.Actors.Generators/XUnitLoggingProvider.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Xunit.Abstractions; + +namespace Dapr.E2E.Test.Actors.Generators; + +internal sealed class XUnitLoggingProvider : ILoggerProvider +{ + private readonly ITestOutputHelper output; + + public XUnitLoggingProvider(ITestOutputHelper output) + { + this.output = output; + } + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(categoryName, this.output); + } + + public void Dispose() + { + } + + private sealed class XUnitLogger : ILogger + { + private readonly string categoryName; + private readonly ITestOutputHelper output; + + public XUnitLogger(string categoryName, ITestOutputHelper output) + { + this.categoryName = categoryName; + this.output = output; + } + +#nullable disable + public IDisposable BeginScope(TState state) + { + return new XUnitLoggerScope(); + } +#nullable enable + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + this.output.WriteLine($"{this.categoryName}: {formatter(state, exception).TrimEnd(Environment.NewLine.ToCharArray())}"); + } + } + + private sealed class XUnitLoggerScope : IDisposable + { + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs index 4c6589965..14b9fef4e 100644 --- a/test/Dapr.E2E.Test.App/Actors/TimerActor.cs +++ b/test/Dapr.E2E.Test.App/Actors/TimerActor.cs @@ -39,7 +39,7 @@ public Task GetState() public async Task StartTimer(StartTimerOptions options) { var bytes = JsonSerializer.SerializeToUtf8Bytes(options, this.Host.JsonSerializerOptions); - await this.RegisterTimerAsync("test-timer", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(50)); + await this.RegisterTimerAsync("test-timer", nameof(Tick), bytes, dueTime: TimeSpan.Zero, period: TimeSpan.FromMilliseconds(100)); await this.StateManager.SetStateAsync("timer-state", new State(){ IsTimerRunning = true, }); } From 1b7c9f4f80df6c42d4d36968e4d5255b72ef7114 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Fri, 8 Mar 2024 09:48:58 -0800 Subject: [PATCH 49/84] Merge 1.13 release branch back to master (#1247) * 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 * Make final 1.13 changes. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Co-authored-by: Shivam Kumar --- .github/workflows/itests.yml | 6 +-- .github/workflows/sdk_build.yml | 4 +- src/Dapr.Client/DaprClientGrpc.cs | 6 +-- src/Dapr.Client/DaprMetadata.cs | 1 + .../Protos/dapr/proto/dapr/v1/dapr.proto | 54 +++++++++++++------ src/Dapr.Workflow/Dapr.Workflow.csproj | 4 +- src/Dapr.Workflow/DaprWorkflowClient.cs | 20 ++++--- .../DaprClientTest.InvokeMethodGrpcAsync.cs | 5 +- 8 files changed, 65 insertions(+), 35 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 3267783c0..2c97343ed 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -42,9 +42,9 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.12.0 - DAPR_RUNTIME_VER: 1.12.0 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.12/install/install.sh + DAPR_CLI_VER: 1.13.0 + DAPR_RUNTIME_VER: 1.13.0 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.13/install/install.sh DAPR_CLI_REF: '' steps: - name: Set up Dapr CLI diff --git a/.github/workflows/sdk_build.yml b/.github/workflows/sdk_build.yml index 4fde80610..fe935bfb8 100644 --- a/.github/workflows/sdk_build.yml +++ b/.github/workflows/sdk_build.yml @@ -32,7 +32,7 @@ jobs: - name: Generate Packages run: dotnet pack --configuration release - name: Upload packages - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v4 with: name: packages path: ${{ env.NUPKG_OUTDIR }} @@ -116,7 +116,7 @@ jobs: if: startswith(github.ref, 'refs/tags/v') && !(endsWith(github.ref, '-rc') || endsWith(github.ref, '-dev') || endsWith(github.ref, '-prerelease')) steps: - name: Download release artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: packages path: packages diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index f856b87e6..3cd7de526 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -2340,7 +2340,7 @@ public override async Task WaitForSidecarAsync(CancellationToken cancellationTok /// public async override Task ShutdownSidecarAsync(CancellationToken cancellationToken = default) { - await client.ShutdownAsync(new Empty(), CreateCallOptions(null, cancellationToken)); + await client.ShutdownAsync(new Autogenerated.ShutdownRequest(), CreateCallOptions(null, cancellationToken)); } /// @@ -2349,9 +2349,9 @@ public override async Task GetMetadataAsync(CancellationToken canc var options = CreateCallOptions(headers: null, cancellationToken); try { - var response = await client.GetMetadataAsync(new Empty(), options); + var response = await client.GetMetadataAsync(new Autogenerated.GetMetadataRequest(), options); return new DaprMetadata(response.Id, - response.ActiveActorsCount.Select(c => new DaprActorMetadata(c.Type, c.Count)).ToList(), + 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()); } diff --git a/src/Dapr.Client/DaprMetadata.cs b/src/Dapr.Client/DaprMetadata.cs index 4cd812e04..a58707c99 100644 --- a/src/Dapr.Client/DaprMetadata.cs +++ b/src/Dapr.Client/DaprMetadata.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; using System.Collections.Generic; namespace Dapr.Client diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto index eafb5452e..5ec1cc9d8 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto @@ -79,9 +79,6 @@ service Dapr { // Unregister an actor reminder. rpc UnregisterActorReminder(UnregisterActorReminderRequest) returns (google.protobuf.Empty) {} - // Rename an actor reminder. - rpc RenameActorReminder(RenameActorReminderRequest) returns (google.protobuf.Empty) {} - // Gets the state for a specific actor. rpc GetActorState(GetActorStateRequest) returns (GetActorStateResponse) {} @@ -122,7 +119,7 @@ service Dapr { rpc DecryptAlpha1(stream DecryptRequest) returns (stream DecryptResponse); // Gets metadata of the sidecar - rpc GetMetadata (google.protobuf.Empty) returns (GetMetadataResponse) {} + rpc GetMetadata (GetMetadataRequest) returns (GetMetadataResponse) {} // Sets value in extended metadata of the sidecar rpc SetMetadata (SetMetadataRequest) returns (google.protobuf.Empty) {} @@ -190,7 +187,7 @@ service Dapr { // Raise an event to a running workflow instance rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} // Shutdown the sidecar - rpc Shutdown (google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -407,7 +404,6 @@ message BulkPublishResponse { // BulkPublishResponseFailedEntry is the message containing the entryID and error of a failed event in BulkPublishEvent call message BulkPublishResponseFailedEntry { - // The response scoped unique ID referring to this message string entry_id = 1; @@ -415,7 +411,6 @@ message BulkPublishResponseFailedEntry { string error = 2; } - // InvokeBindingRequest is the message to send data to output bindings message InvokeBindingRequest { // The name of the output binding to invoke. @@ -544,14 +539,6 @@ message UnregisterActorReminderRequest { string name = 3; } -// RenameActorReminderRequest is the message to rename an actor reminder. -message RenameActorReminderRequest { - string actor_type = 1; - string actor_id = 2; - string old_name = 3; - string new_name = 4; -} - // GetActorStateRequest is the message to get key-value states from specific actor. message GetActorStateRequest { string actor_type = 1; @@ -600,10 +587,16 @@ message InvokeActorResponse { bytes data = 1; } -// GetMetadataResponse is a message that is returned on GetMetadata rpc call +// GetMetadataRequest is the message for the GetMetadata request. +message GetMetadataRequest { + // Empty +} + +// GetMetadataResponse is a message that is returned on GetMetadata rpc call. message GetMetadataResponse { string id = 1; - repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors"]; + // Deprecated alias for actor_runtime.active_actors. + repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors", deprecated = true]; repeated RegisteredComponents registered_components = 3 [json_name = "components"]; map extended_metadata = 4 [json_name = "extended"]; repeated PubsubSubscription subscriptions = 5 [json_name = "subscriptions"]; @@ -611,6 +604,28 @@ message GetMetadataResponse { AppConnectionProperties app_connection_properties = 7 [json_name = "appConnectionProperties"]; string runtime_version = 8 [json_name = "runtimeVersion"]; repeated string enabled_features = 9 [json_name = "enabledFeatures"]; + ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; +} + +message ActorRuntime { + enum ActorRuntimeStatus { + // Indicates that the actor runtime is still being initialized. + INITIALIZING = 0; + // Indicates that the actor runtime is disabled. + // This normally happens when Dapr is started without "placement-host-address" + DISABLED = 1; + // Indicates the actor runtime is running, either as an actor host or client. + RUNNING = 2; + } + + // Contains an enum indicating whether the actor runtime has been initialized. + ActorRuntimeStatus runtime_status = 1 [json_name = "runtimeStatus"]; + // Count of active actors per type. + repeated ActiveActorsCount active_actors = 2 [json_name = "activeActors"]; + // Indicates whether the actor runtime is ready to host actors. + bool host_ready = 3 [json_name = "hostReady"]; + // Custom message from the placement provider. + string placement = 4 [json_name = "placement"]; } message ActiveActorsCount { @@ -1088,3 +1103,8 @@ message PurgeWorkflowRequest { // Name of the workflow component. string workflow_component = 2 [json_name = "workflowComponent"]; } + +// ShutdownRequest is the request for Shutdown. +message ShutdownRequest { + // Empty +} diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index d5820deb1..9092b101a 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/Dapr.Workflow/DaprWorkflowClient.cs b/src/Dapr.Workflow/DaprWorkflowClient.cs index 4c4902dbb..e4c88f0ef 100644 --- a/src/Dapr.Workflow/DaprWorkflowClient.cs +++ b/src/Dapr.Workflow/DaprWorkflowClient.cs @@ -158,10 +158,10 @@ public async Task WaitForWorkflowCompletionAsync( /// the terminated state. /// /// - /// Terminating a workflow instance has no effect on any in-flight activity function executions - /// or child workflows that were started by the terminated instance. Those actions will continue to run - /// without interruption. However, their results will be discarded. If you want to terminate child-workflows, - /// you must issue separate terminate commands for each child workflow instance individually. + /// Terminating a workflow terminates all of the child workflow instances that were created by the target. But it + /// has no effect on any in-flight activity function executions + /// that were started by the terminated instance. Those actions will continue to run + /// without interruption. However, their results will be discarded. /// /// At the time of writing, there is no way to terminate an in-flight activity execution. /// @@ -178,7 +178,11 @@ public Task TerminateWorkflowAsync( string? output = null, CancellationToken cancellation = default) { - return this.innerClient.TerminateInstanceAsync(instanceId, output, cancellation); + TerminateInstanceOptions options = new TerminateInstanceOptions { + Output = output, + Recursive = true, + }; + return this.innerClient.TerminateInstanceAsync(instanceId, options, cancellation); } /// @@ -269,6 +273,9 @@ public Task ResumeWorkflowAsync( /// , , or /// state can be purged. /// + /// + /// Purging a workflow purges all of the child workflows that were created by the target. + /// /// /// The unique ID of the workflow instance to purge. /// @@ -280,7 +287,8 @@ public Task ResumeWorkflowAsync( /// public async Task PurgeInstanceAsync(string instanceId, CancellationToken cancellation = default) { - PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, cancellation); + PurgeInstanceOptions options = new PurgeInstanceOptions {Recursive = true}; + PurgeResult result = await this.innerClient.PurgeInstanceAsync(instanceId, options, cancellation); return result.PurgedInstanceCount > 0; } diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs index 5412c4063..4001e4b06 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodGrpcAsync.cs @@ -368,7 +368,7 @@ public async Task GetMetadataAsync_WrapsRpcException() var rpcException = new RpcException(rpcStatus, new Metadata(), rpcExceptionMessage); client.Mock - .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) + .Setup(m => m.GetMetadataAsync(It.IsAny(), It.IsAny())) .Throws(rpcException); var ex = await Assert.ThrowsAsync(async () => @@ -395,9 +395,10 @@ public async Task GetMetadataAsync_WithReturnTypeAndData() // Create Response & Respond var response = new Autogen.Grpc.v1.GetMetadataResponse() { + ActorRuntime = new(), Id = "testId", }; - response.ActiveActorsCount.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); + response.ActorRuntime.ActiveActors.Add(new ActiveActorsCount { Type = "testType", Count = 1 }); response.RegisteredComponents.Add(new RegisteredComponents { Name = "testName", Type = "testType", Version = "V1" }); response.ExtendedMetadata.Add("e1", "v1"); From 31af35b6c6df4144871a8933c4d42b7e36090ce0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 8 Apr 2024 11:52:09 -0500 Subject: [PATCH 50/84] Updated to reflect latest guidance to register endpoints via top-level route registrations (#1262) Signed-off-by: Whit Waldo --- .../dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 fdff16ffc..8229d6820 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 @@ -319,11 +319,8 @@ namespace MyActorService app.UseRouting(); - app.UseEndpoints(endpoints => - { - // Register actors handlers that interface with the Dapr runtime. - endpoints.MapActorsHandlers(); - }); + // Register actors handlers that interface with the Dapr runtime. + app.MapActorsHandlers(); } } } From bdca3b320bb9695e5caa2e68226cb930e8487d0e Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Mon, 8 Apr 2024 18:23:49 +0100 Subject: [PATCH 51/84] Adds an option to set a timeout for service invocation (#1252) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds http timeout Signed-off-by: Elena Kolevska * Adds a timeout for the grpc client Signed-off-by: Elena Kolevska * Small updates Signed-off-by: Elena Kolevska * Updates test Signed-off-by: Elena Kolevska * Adds a timeout example in docs Signed-off-by: Elena Kolevska * Adds e2e test for http service invocation Signed-off-by: Elena Kolevska * Adds tests for grpc service invocation Signed-off-by: Elena Kolevska * Removes grpc timeout, because it’s not needed. It can be passed directly to the call as shown in the updated tests and docs Signed-off-by: Elena Kolevska * Update src/Dapr.Client/DaprClientBuilder.cs Signed-off-by: Elena Kolevska --------- Signed-off-by: Elena Kolevska Signed-off-by: Elena Kolevska Co-authored-by: Phillip Hoff --- .../dotnet-sdk-docs/dotnet-client/_index.md | 25 +++++++++++++++++-- src/Dapr.Client/DaprClientBuilder.cs | 21 +++++++++++++++- .../DaprClientBuilderTest.cs | 11 ++++++++ .../Proto/message.proto | 1 + .../Services/MessagerService.cs | 7 ++++++ .../Controllers/TestController.cs | 9 +++++++ .../E2ETests.GrpcProxyInvocationTests.cs | 17 +++++++++++++ .../E2ETests.ServiceInvocationTests.cs | 24 ++++++++++++++++++ 8 files changed, 112 insertions(+), 3 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index 41e610125..f608cd07d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -21,13 +21,16 @@ The .NET SDK allows you to interface with all of the [Dapr building blocks]({{< ### Invoke a service +#### HTTP You can either use the `DaprClient` or `System.Net.Http.HttpClient` to invoke your services. {{< tabs SDK HTTP>}} {{% codetab %}} ```csharp -using var client = new DaprClientBuilder().Build(); +using var client = new DaprClientBuilder(). + UseTimeout(TimeSpan.FromSeconds(2)). // Optionally, set a timeout + Build(); // Invokes a POST method named "deposit" that takes input of type "Transaction" var data = new { id = "17", amount = 99m }; @@ -40,15 +43,33 @@ Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance) ```csharp var client = DaprClient.CreateInvokeHttpClient(appId: "routing"); +// To set a timeout on the HTTP client: +client.Timeout = TimeSpan.FromSeconds(2); + var deposit = new Transaction { Id = "17", Amount = 99m }; var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken); var account = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance); ``` {{% /codetab %}} - {{< /tabs >}} +#### gRPC +You can use the `DaprClient` to invoke your services over gRPC. +{{% codetab %}} +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); +var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint); +var client = new MyService.MyServiceClient(invoker); + +var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); +await client.MyMethodAsync(new Empty(), options); + +Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); +``` +{{% /codetab %}} + + - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). ### Save & get application state diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 1580afb36..50a4979d1 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -57,6 +57,7 @@ public DaprClientBuilder() // property exposed for testing purposes internal GrpcChannelOptions GrpcChannelOptions { get; private set; } internal string DaprApiToken { get; private set; } + internal TimeSpan Timeout { get; private set; } /// /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. @@ -136,6 +137,17 @@ public DaprClientBuilder UseDaprApiToken(string apiToken) return this; } + /// + /// Sets the timeout for the HTTP client used by the . + /// + /// + /// + public DaprClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + /// /// Builds a instance from the properties of the builder. /// @@ -162,9 +174,16 @@ public DaprClient Build() var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); var client = new Autogenerated.Dapr.DaprClient(channel); - + + var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); + + if (this.Timeout > TimeSpan.Zero) + { + httpClient.Timeout = this.Timeout; + } + return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs index 2da50922a..52d0b7000 100644 --- a/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprClientBuilderTest.cs @@ -14,6 +14,7 @@ using System; using System.Text.Json; using Dapr.Client; +using Grpc.Core; using Grpc.Net.Client; using Xunit; @@ -110,5 +111,15 @@ public void DaprClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() var entry = DaprClient.GetDaprApiTokenHeader(builder.DaprApiToken); Assert.Equal(default, entry); } + + [Fact] + public void DaprClientBuilder_SetsTimeout() + { + var builder = new DaprClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } } + } diff --git a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto index a8f67f578..3b73b73a9 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto +++ b/test/Dapr.E2E.Test.App.Grpc/Proto/message.proto @@ -25,6 +25,7 @@ service Messager { rpc GetMessage(GetMessageRequest) returns (MessageResponse); // Send a series of broadcast messages. rpc StreamBroadcast(stream Broadcast) returns (stream MessageResponse); + rpc DelayedResponse(google.protobuf.Empty) returns (google.protobuf.Empty); } message SendMessageRequest { diff --git a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs index 0e22f3eec..950668932 100644 --- a/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs +++ b/test/Dapr.E2E.Test.App.Grpc/Services/MessagerService.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System; using System.Threading.Tasks; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -44,5 +45,11 @@ public override async Task StreamBroadcast(IAsyncStreamReader request await responseStream.WriteAsync(new MessageResponse { Message = request.Message }); } } + + public override async Task DelayedResponse(Empty request, ServerCallContext context) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return new Empty(); + } } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test.App/Controllers/TestController.cs b/test/Dapr.E2E.Test.App/Controllers/TestController.cs index 1f26ce809..4e475c197 100644 --- a/test/Dapr.E2E.Test.App/Controllers/TestController.cs +++ b/test/Dapr.E2E.Test.App/Controllers/TestController.cs @@ -65,5 +65,14 @@ public ActionResult AccountDetailsRequiresApiToken(Transaction transact }; return account; } + + [Authorize("Dapr")] + [HttpGet("DelayedResponse")] + public async Task DelayedResponse() + { + await Task.Delay(TimeSpan.FromSeconds(2)); + return Ok(); + } + } } diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs index 0f991a9c7..3b6f31e84 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.GrpcProxyInvocationTests.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Dapr.Client; +using Google.Protobuf.WellKnownTypes; using Grpc.Core; using Xunit; using Xunit.Abstractions; @@ -77,5 +78,21 @@ public async Task TestGrpcProxyStreamingBroadcast() await responseTask; } } + + [Fact] + public async Task TestGrpcServiceInvocationWithTimeout() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + var invoker = DaprClient.CreateInvocationInvoker(appId: this.AppId, daprEndpoint: this.GrpcEndpoint); + var client = new Messager.MessagerClient(invoker); + + var options = new CallOptions(cancellationToken: cts.Token, deadline: DateTime.UtcNow.AddSeconds(1)); + var ex = await Assert.ThrowsAsync(async () => + { + await client.DelayedResponseAsync(new Empty(), options); + }); + + Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); + } } } \ No newline at end of file diff --git a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs index cdc6170ce..da49e6721 100644 --- a/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs +++ b/test/Dapr.E2E.Test/ServiceInvocation/E2ETests.ServiceInvocationTests.cs @@ -13,10 +13,15 @@ namespace Dapr.E2E.Test { using System; + using System.Net; + using System.Net.Http; using System.Net.Http.Json; + using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Dapr.Client; + using Google.Protobuf.WellKnownTypes; + using Grpc.Core; using Xunit; public partial class E2ETests @@ -58,6 +63,25 @@ public async Task TestServiceInvocationRequiresApiToken() Assert.Equal("1", account.Id); Assert.Equal(150, account.Balance); } + + [Fact] + public async Task TestHttpServiceInvocationWithTimeout() + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); + using var client = new DaprClientBuilder() + .UseHttpEndpoint(this.HttpEndpoint) + .UseTimeout(TimeSpan.FromSeconds(1)) + .Build(); + + await Assert.ThrowsAsync(async () => + { + await client.InvokeMethodAsync( + appId: this.AppId, + methodName: "DelayedResponse", + httpMethod: new HttpMethod("GET"), + cancellationToken: cts.Token); + }); + } } internal class Transaction From ca0bffa4409e71203ba9b75dde5ae5e71858121f Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 8 Apr 2024 12:33:55 -0500 Subject: [PATCH 52/84] Updating Workflow XML comment for accuracy (#1260) Signed-off-by: Whit Waldo Co-authored-by: Phillip Hoff --- src/Dapr.Workflow/WorkflowContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index f4e500134..98b8be96b 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -245,9 +245,9 @@ public abstract Task CallChildWorkflowAsync( /// exception will be surfaced to the parent workflow, just like it is when an activity task fails with an /// exception. Child workflows also support automatic retry policies. /// - /// Because child workflows are independent of their parents, terminating a parent workflow does not affect - /// any child workflows. You must terminate each child workflow independently using its instance ID, which - /// is specified by . + /// Terminating a parent workflow terminates all the child workflows created by the workflow instance. See the documentation at + /// https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#child-workflows regarding + /// the terminate workflow API for more information. /// /// /// The name of the workflow to call. From 64c2f4809b05ebc1a24a165ec9b7431e4eafe219 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 8 Apr 2024 12:56:19 -0500 Subject: [PATCH 53/84] Updated property on type to reflect the fact that it can return a null value (which it will if a key doesn't have any data in the state store). Only enabled nullable annotation on this file for now. (#1259) Signed-off-by: Whit Waldo Co-authored-by: Phillip Hoff --- src/Dapr.Client/StateQueryResponse.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Dapr.Client/StateQueryResponse.cs b/src/Dapr.Client/StateQueryResponse.cs index 8ef4a65f0..c1cc87386 100644 --- a/src/Dapr.Client/StateQueryResponse.cs +++ b/src/Dapr.Client/StateQueryResponse.cs @@ -11,6 +11,7 @@ You may obtain a copy of the License at limitations under the License. */ +#nullable enable using System.Collections.Generic; namespace Dapr.Client @@ -75,9 +76,9 @@ public StateQueryItem(string key, TValue data, string etag, string error) public string Key { get; } /// - /// The data of the the key from the matched query. + /// The data of the key from the matched query. /// - public TValue Data { get; } + public TValue? Data { get; } /// /// The ETag for the key from the matched query. From ad3350f70ebca14cdad5f3bdff5c1b4c6a1bf890 Mon Sep 17 00:00:00 2001 From: James Thompson Date: Tue, 9 Apr 2024 04:13:33 +1000 Subject: [PATCH 54/84] #1239 remove polyfill packages (#1258) Signed-off-by: James Thompson - SkiData Co-authored-by: Phillip Hoff --- src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj | 6 ------ src/Dapr.AspNetCore/Dapr.AspNetCore.csproj | 7 ------- src/Dapr.Client/Dapr.Client.csproj | 1 - .../Dapr.Actors.AspNetCore.Test.csproj | 1 - 4 files changed, 15 deletions(-) diff --git a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj index 1114b7828..82c5863db 100644 --- a/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj +++ b/src/Dapr.Actors.AspNetCore/Dapr.Actors.AspNetCore.csproj @@ -1,12 +1,6 @@  - - diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index c61fc5abc..54996e4bc 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -12,13 +12,6 @@ - - - diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index a3fd5b082..1a348cc86 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -20,7 +20,6 @@ - diff --git a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj index 7e352d007..83ea494a8 100644 --- a/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj +++ b/test/Dapr.Actors.AspNetCore.Test/Dapr.Actors.AspNetCore.Test.csproj @@ -11,7 +11,6 @@ - From ba1341510bfc81f0e1ba4687539ec49aa90abc4e Mon Sep 17 00:00:00 2001 From: Carlos Mendible <266546+cmendible@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:53:51 +0200 Subject: [PATCH 55/84] Updated .github/holopin.yml. Fixes #1270 (#1276) Signed-off-by: Carlos Mendible <266546+cmendible@users.noreply.github.com> --- .github/holopin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/holopin.yml b/.github/holopin.yml index 44a7f0c8a..7472cc284 100644 --- a/.github/holopin.yml +++ b/.github/holopin.yml @@ -1,6 +1,6 @@ organization: dapr -defaultSticker: clmjkxscc122740fl0mkmb7egi +defaultSticker: clrqfdv4x24910fl5n4iwu5oa stickers: - - id: clmjkxscc122740fl0mkmb7egi - alias: ghc2023 + id: clrqfdv4x24910fl5n4iwu5oa + alias: sdk-badge \ No newline at end of file From fd71812dc5e745d9ca62b8f45ed503c9996a3d6b Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Mon, 13 May 2024 18:41:46 +0200 Subject: [PATCH 56/84] Update README.md (#1284) Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index d7232c54e..e14c9400e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ -# Dapr SDK for .NET - -[![Build Status](https://github.com/dapr/dotnet-sdk/workflows/build/badge.svg)](https://github.com/dapr/dotnet-sdk/actions?workflow=build) -[![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) -[![License: Apache](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) -[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgit%40github.com%3Adapr%2Fdotnet-sdk.git?ref=badge_shield) +[![NuGet Version](https://img.shields.io/nuget/v/Dapr.Client?logo=nuget&label=Latest%20version&style=flat)]((https://www.nuget.org/packages/Dapr.Client)) [![NuGet Downloads](https://img.shields.io/nuget/dt/Dapr.Client?style=flat&logo=nuget&label=Downloads)](https://www.nuget.org/packages/Dapr.Client) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/dapr/dotnet-sdk/.github%2Fworkflows%2Fsdk_build.yml?branch=master&label=Build&logo=github) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) ![GitHub License](https://img.shields.io/github/license/dapr/dotnet-sdk?style=flat&label=License&logo=github) [![GitHub issue custom search in repo](https://img.shields.io/github/issues-search/dapr/dotnet-sdk?query=type%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&label=Good%20first%20issues&style=flat&logo=github)](https://github.com/dapr/dotnet-sdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) [![Discord](https://img.shields.io/discord/778680217417809931?label=Discord&style=flat&logo=discord)](http://bit.ly/dapr-discord) [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCtpSQ9BLB_3EXdWAUQYwnRA?style=flat&label=YouTube%20views&logo=youtube)](https://youtube.com/@daprdev) ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/daprdev?logo=x&style=flat) Dapr SDK for .NET allows you to: From 23c484e1f077dcdfed8747c46eaf86c04434ec58 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Tue, 14 May 2024 19:08:06 +0200 Subject: [PATCH 57/84] restored missing title in readme (#1286) Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e14c9400e..053320090 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Dapr SDK for .NET + [![NuGet Version](https://img.shields.io/nuget/v/Dapr.Client?logo=nuget&label=Latest%20version&style=flat)]((https://www.nuget.org/packages/Dapr.Client)) [![NuGet Downloads](https://img.shields.io/nuget/dt/Dapr.Client?style=flat&logo=nuget&label=Downloads)](https://www.nuget.org/packages/Dapr.Client) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/dapr/dotnet-sdk/.github%2Fworkflows%2Fsdk_build.yml?branch=master&label=Build&logo=github) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) ![GitHub License](https://img.shields.io/github/license/dapr/dotnet-sdk?style=flat&label=License&logo=github) [![GitHub issue custom search in repo](https://img.shields.io/github/issues-search/dapr/dotnet-sdk?query=type%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&label=Good%20first%20issues&style=flat&logo=github)](https://github.com/dapr/dotnet-sdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) [![Discord](https://img.shields.io/discord/778680217417809931?label=Discord&style=flat&logo=discord)](http://bit.ly/dapr-discord) [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCtpSQ9BLB_3EXdWAUQYwnRA?style=flat&label=YouTube%20views&logo=youtube)](https://youtube.com/@daprdev) ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/daprdev?logo=x&style=flat) From 190156f212214826c040e3a0f2b1b6e87b7f9b25 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Wed, 22 May 2024 06:25:19 +0200 Subject: [PATCH 58/84] Fixed badge broken links (#1290) * Fixed badge broken links Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> * Update README.md Co-authored-by: Marc Duiker Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> --------- Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Co-authored-by: Marc Duiker --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 053320090..948516fe2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dapr SDK for .NET -[![NuGet Version](https://img.shields.io/nuget/v/Dapr.Client?logo=nuget&label=Latest%20version&style=flat)]((https://www.nuget.org/packages/Dapr.Client)) [![NuGet Downloads](https://img.shields.io/nuget/dt/Dapr.Client?style=flat&logo=nuget&label=Downloads)](https://www.nuget.org/packages/Dapr.Client) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/dapr/dotnet-sdk/.github%2Fworkflows%2Fsdk_build.yml?branch=master&label=Build&logo=github) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) ![GitHub License](https://img.shields.io/github/license/dapr/dotnet-sdk?style=flat&label=License&logo=github) [![GitHub issue custom search in repo](https://img.shields.io/github/issues-search/dapr/dotnet-sdk?query=type%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&label=Good%20first%20issues&style=flat&logo=github)](https://github.com/dapr/dotnet-sdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) [![Discord](https://img.shields.io/discord/778680217417809931?label=Discord&style=flat&logo=discord)](http://bit.ly/dapr-discord) [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCtpSQ9BLB_3EXdWAUQYwnRA?style=flat&label=YouTube%20views&logo=youtube)](https://youtube.com/@daprdev) ![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/daprdev?logo=x&style=flat) +[![NuGet Version](https://img.shields.io/nuget/v/Dapr.Client?logo=nuget&label=Latest%20version&style=flat)](https://www.nuget.org/packages/Dapr.Client) [![NuGet Downloads](https://img.shields.io/nuget/dt/Dapr.Client?style=flat&logo=nuget&label=Downloads)](https://www.nuget.org/packages/Dapr.Client) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/dapr/dotnet-sdk/.github%2Fworkflows%2Fsdk_build.yml?branch=master&label=Build&logo=github)](https://github.com/dapr/dotnet-sdk/actions/workflows/sdk_build.yml) [![codecov](https://codecov.io/gh/dapr/dotnet-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/dapr/dotnet-sdk) [![GitHub License](https://img.shields.io/github/license/dapr/dotnet-sdk?style=flat&label=License&logo=github)](https://github.com/dapr/dotnet-sdk/blob/master/LICENSE) [![GitHub issue custom search in repo](https://img.shields.io/github/issues-search/dapr/dotnet-sdk?query=type%3Aissue%20is%3Aopen%20label%3A%22good%20first%20issue%22&label=Good%20first%20issues&style=flat&logo=github)](https://github.com/dapr/dotnet-sdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) [![Discord](https://img.shields.io/discord/778680217417809931?label=Discord&style=flat&logo=discord)](http://bit.ly/dapr-discord) [![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCtpSQ9BLB_3EXdWAUQYwnRA?style=flat&label=YouTube%20views&logo=youtube)](https://youtube.com/@daprdev) [![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/daprdev?logo=x&style=flat)](https://twitter.com/daprdev) Dapr SDK for .NET allows you to: From fba9dfd531a0c100ca93f4d4ba66ffcda7bebc8b Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Thu, 6 Jun 2024 00:31:41 +0200 Subject: [PATCH 59/84] Removed non-existent project, correct path of the generator project. (#1297) Signed-off-by: Manuel Menegazzo --- all.sln | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/all.sln b/all.sln index be5620b64..0b95478f3 100644 --- a/all.sln +++ b/all.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32929.385 @@ -71,8 +70,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StateManagement", "examples EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceInvocation", "examples\Client\ServiceInvocation\ServiceInvocation.csproj", "{8B570E70-0E73-4042-A4B6-1CC3CC782A65}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublishSubscribe", "examples\Client\PublishSubscribe\PublishSubscribe.csproj", "{DE6913E3-E5D9-4D1D-95F9-9FED87BD09BC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test", "test\Dapr.E2E.Test\Dapr.E2E.Test.csproj", "{4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.App", "test\Dapr.E2E.Test.App\Dapr.E2E.Test.App.csproj", "{345FC3FB-D1E9-4AE8-9052-17D20AB01FA2}" @@ -106,18 +103,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "example EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GeneratedActor", "GeneratedActor", "{7592AFA4-426B-42F3-AE82-957C86814482}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorClient", "examples\GeneratedActor\ActorClient\ActorClient.csproj", "{61C24126-F39D-4BEA-96DC-FC87BA730554}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorClient", "examples\GeneratedActor\ActorClient\ActorClient.csproj", "{61C24126-F39D-4BEA-96DC-FC87BA730554}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorCommon", "examples\GeneratedActor\ActorCommon\ActorCommon.csproj", "{CB903D21-4869-42EF-BDD6-5B1CFF674337}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorCommon", "examples\GeneratedActor\ActorCommon\ActorCommon.csproj", "{CB903D21-4869-42EF-BDD6-5B1CFF674337}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.Generators", "src\Dapr.Actors.Generators\Dapr.Actors.Generators.csproj", "{980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Generators", "src\Dapr.Actors.Generators\Dapr.Actors.Generators.csproj", "{980B5FD8-0107-41F7-8FAD-E4E8BAE8A625}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActorService", "examples\GeneratedActor\ActorService\ActorService.csproj", "{7C06FE2D-6C62-48F5-A505-F0D715C554DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActorService", "examples\GeneratedActor\ActorService\ActorService.csproj", "{7C06FE2D-6C62-48F5-A505-F0D715C554DE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.Generators.Test", "test\Dapr.Actors.Generators.Test\Dapr.Actors.Generators.Test.csproj", "{AF89083D-4715-42E6-93E9-38497D12A8A6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Actors.Generators.Test", "test\Dapr.Actors.Generators.Test\Dapr.Actors.Generators.Test.csproj", "{AF89083D-4715-42E6-93E9-38497D12A8A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Generators", "test\Dapr.E2E.Test.Actors.Generators\Dapr.E2E.Test.Actors.Generators.csproj", "{B5CDB0DC-B26D-48F1-B934-FE5C1C991940}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.E2E.Test.Actors.Generators", "test\Dapr.E2E.Test.Actors.Generators\Dapr.E2E.Test.Actors.Generators.csproj", "{B5CDB0DC-B26D-48F1-B934-FE5C1C991940}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -291,10 +289,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 - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release - {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,7 +337,7 @@ Global {7592AFA4-426B-42F3-AE82-957C86814482} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {61C24126-F39D-4BEA-96DC-FC87BA730554} = {7592AFA4-426B-42F3-AE82-957C86814482} {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {7592AFA4-426B-42F3-AE82-957C86814482} - {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {7592AFA4-426B-42F3-AE82-957C86814482} + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {7592AFA4-426B-42F3-AE82-957C86814482} {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} @@ -353,3 +347,16 @@ Global SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} EndGlobalSection EndGlobal +8-446B-AECD-DCC2CC871F73} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} + EndGlobalSection +EndGlobal +C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} + EndGlobalSection +EndGlobal From 2e94bb190597d93fc612fd50e2e159637bcf840c Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 25 Jun 2024 14:30:18 -0500 Subject: [PATCH 60/84] 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 --- .../DaprServiceCollectionExtensions.cs | 30 ++++++++++++++----- .../DaprServiceCollectionExtensionsTest.cs | 30 +++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 8491cb9b2..388015b80 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -31,18 +31,32 @@ public static class DaprServiceCollectionExtensions /// public static void AddDaprClient(this IServiceCollection services, Action configure = null) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } + ArgumentNullException.ThrowIfNull(services, nameof(services)); services.TryAddSingleton(_ => { var builder = new DaprClientBuilder(); - if (configure != null) - { - configure.Invoke(builder); - } + configure?.Invoke(builder); + + 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) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + services.TryAddSingleton(serviceProvider => + { + var builder = new DaprClientBuilder(); + configure?.Invoke(serviceProvider, builder); return builder.Build(); }); diff --git a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs index 614faf5e4..a82948cf3 100644 --- a/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs +++ b/test/Dapr.AspNetCore.Test/DaprServiceCollectionExtensionsTest.cs @@ -48,6 +48,31 @@ public void AddDaprClient_RegistersDaprClientOnlyOnce() Assert.True(daprClient.JsonSerializerOptions.PropertyNameCaseInsensitive); } + [Fact] + public void AddDaprClient_RegistersUsingDependencyFromIServiceProvider() + { + + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var caseSensitivity = configProvider.GetCaseSensitivity(); + + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = caseSensitivity + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + + DaprClientGrpc client = serviceProvider.GetRequiredService() as DaprClientGrpc; + + //Registers with case-insensitive as true by default, but we set as false above + Assert.False(client.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + #if NET8_0_OR_GREATER [Fact] public void AddDaprClient_WithKeyedServices() @@ -65,5 +90,10 @@ public void AddDaprClient_WithKeyedServices() Assert.NotNull(daprClient); } #endif + + private class TestConfigurationProvider + { + public bool GetCaseSensitivity() => false; + } } } From 512c9eaaf4dcab826e9ba5a79dfbee6f06671cf7 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Tue, 25 Jun 2024 13:46:25 -0700 Subject: [PATCH 61/84] 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 --- .github/workflows/itests.yml | 4 +-- .../DaprSecretStoreConfigurationProvider.cs | 11 +++++-- ...aprSecretStoreConfigurationProviderTest.cs | 29 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 2c97343ed..00121c9f5 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -43,8 +43,8 @@ jobs: GOARCH: amd64 GOPROXY: https://proxy.golang.org DAPR_CLI_VER: 1.13.0 - DAPR_RUNTIME_VER: 1.13.0 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.13/install/install.sh + DAPR_RUNTIME_VER: 1.13.2 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.12/install/install.sh DAPR_CLI_REF: '' steps: - name: Set up Dapr CLI diff --git a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs index 5991a7dad..ecd0ac91b 100644 --- a/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs +++ b/src/Dapr.Extensions.Configuration/DaprSecretStoreConfigurationProvider.cs @@ -227,8 +227,15 @@ private async Task LoadAsync() $"A duplicate key '{key}' was found in the secret store '{store}'. Please remove any duplicates from your secret store."); } - data.Add(normalizeKey ? NormalizeKey(secretDescriptor.SecretName) : secretDescriptor.SecretName, - result[key]); + // The name of the key "as desired" by the user based on the descriptor. + // + // NOTE: This should vary only if a single secret of the same name is returned. + string desiredKey = StringComparer.Ordinal.Equals(key, secretDescriptor.SecretKey) ? secretDescriptor.SecretName : key; + + // The name of the key normalized based on the configured delimiters. + string normalizedKey = normalizeKey ? NormalizeKey(desiredKey) : desiredKey; + + data.Add(normalizedKey, result[key]); } } diff --git a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs index d35275dd1..9bac31352 100644 --- a/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs +++ b/test/Dapr.Extensions.Configuration.Test/DaprSecretStoreConfigurationProviderTest.cs @@ -198,6 +198,35 @@ public void LoadSecrets_FromSecretStoreThatCanReturnsMultipleValues() config[secondSecretKey].Should().Be(secondSecretValue); } + [Fact] + public void LoadSecrets_FromSecretStoreThatCanReturnsMultivaluedValues() + { + var storeName = "store"; + var parentSecretKey = "connectionStrings"; + var firstSecretKey = "first_secret"; + var secondSecretKey = "second_secret"; + var firstSecretValue = "secret1"; + var secondSecretValue = "secret2"; + + var secretDescriptors = new[] + { + new DaprSecretDescriptor(parentSecretKey) + }; + + var daprClient = new Mock(); + + daprClient.Setup(c => c.GetSecretAsync(storeName, parentSecretKey, + It.IsAny>(), default)) + .ReturnsAsync(new Dictionary { { firstSecretKey, firstSecretValue }, { secondSecretKey, secondSecretValue } }); + + var config = CreateBuilder() + .AddDaprSecretStore(storeName, secretDescriptors, daprClient.Object) + .Build(); + + config[firstSecretKey].Should().Be(firstSecretValue); + config[secondSecretKey].Should().Be(secondSecretValue); + } + [Fact] public void LoadSecrets_FromSecretStoreWithADifferentSecretKeyAndName() { From 84962532f1bec0c646805e630133be3b59b2e26e Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Wed, 26 Jun 2024 18:19:53 +0200 Subject: [PATCH 62/84] 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 --- all.sln | 14 +---- examples/Actor/DemoActor/DemoActor.csproj | 29 ++++++--- examples/Actor/DemoActor/Startup.cs | 2 +- examples/Actor/DemoActor/demo-actor.yaml | 67 ++++++++++++++++++++ examples/Actor/README.md | 77 +++++++++++++++++++++++ 5 files changed, 166 insertions(+), 23 deletions(-) create mode 100644 examples/Actor/DemoActor/demo-actor.yaml diff --git a/all.sln b/all.sln index 0b95478f3..228047852 100644 --- a/all.sln +++ b/all.sln @@ -32,6 +32,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BD1276E-D28A-45EA-89B1-6AD48471500D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + README.md = README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Actors.AspNetCore.Test", "test\Dapr.Actors.AspNetCore.Test\Dapr.Actors.AspNetCore.Test.csproj", "{9C1D6ABA-5EDE-4FA0-A8A9-0AB98CB74737}" @@ -347,16 +348,3 @@ Global SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} EndGlobalSection EndGlobal -8-446B-AECD-DCC2CC871F73} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} - EndGlobalSection -EndGlobal -C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} - EndGlobalSection -EndGlobal diff --git a/examples/Actor/DemoActor/DemoActor.csproj b/examples/Actor/DemoActor/DemoActor.csproj index 1ee37fdbe..24a42ee0e 100644 --- a/examples/Actor/DemoActor/DemoActor.csproj +++ b/examples/Actor/DemoActor/DemoActor.csproj @@ -1,13 +1,24 @@  - - net6 - - - - - - - + + net6 + + + + true + true + demo-actor + + + + + + + + + + + + diff --git a/examples/Actor/DemoActor/Startup.cs b/examples/Actor/DemoActor/Startup.cs index c04dfdcba..da2b9e764 100644 --- a/examples/Actor/DemoActor/Startup.cs +++ b/examples/Actor/DemoActor/Startup.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/examples/Actor/DemoActor/demo-actor.yaml b/examples/Actor/DemoActor/demo-actor.yaml new file mode 100644 index 000000000..99a8abd34 --- /dev/null +++ b/examples/Actor/DemoActor/demo-actor.yaml @@ -0,0 +1,67 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: statestore +spec: + type: state.in-memory + version: v1 + metadata: + - name: actorStateStore + value: "true" +--- +kind: Service +apiVersion: v1 +metadata: + name: demoactor + labels: + app: demoactor +spec: + selector: + app: demoactor + ports: + - name: app-port + protocol: TCP + port: 5010 + targetPort: app-port + - name: dapr-http + protocol: TCP + port: 3500 + targetPort: 3500 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: demoactor + labels: + app: demoactor +spec: + replicas: 1 + selector: + matchLabels: + app: demoactor + template: + metadata: + labels: + app: demoactor + annotations: + dapr.io/enabled: "true" + dapr.io/app-id: "demoactor" + dapr.io/app-port: "5010" + dapr.io/enable-api-logging: "true" + dapr.io/sidecar-listen-addresses: "0.0.0.0" + spec: + containers: + - name: demoactor + # image: /demo-actor:latest + image: demo-actor:latest + # if you are using docker desktop, you can use imagePullPolicy: Never to use local image + imagePullPolicy: Never + env: + - name: APP_PORT + value: "5010" + - name: ASPNETCORE_URLS + value: "http://+:5010" + ports: + - name: app-port + containerPort: 5010 diff --git a/examples/Actor/README.md b/examples/Actor/README.md index ddc42cecf..a7bb46c03 100644 --- a/examples/Actor/README.md +++ b/examples/Actor/README.md @@ -80,3 +80,80 @@ On Windows: ```sh curl -X POST http://127.0.0.1:3500/v1.0/actors/DemoActor/abc/method/GetData ``` + +### Build and push Docker image +You can build the docker image of `DemoActor` service by running the following commands in the `DemoActor` project directory: + +``` Bash +dotnet publish --os linux --arch x64 /t:PublishContainer -p ContainerImageTags='"latest"' --self-contained +``` + +The build produce and image with tag `demo-actor:latest` and load it in the local registry. +Now the image can be pushed to your remote Docker registry by running the following commands: + +``` Bash +# Replace with your Docker registry +docker tag demo-actor:latest /demo-actor:latest + +# Push the image to your Docker registry +docker push /demo-actor:latest +``` + +### Deploy the Actor service to Kubernetes +#### Prerequisites +- A Kubernetes cluster with `kubectl` configured to access it. +- Dapr v1.13+ installed on the Kubernetes cluster. Follow the instructions [here](https://docs.dapr.io/getting-started/install-dapr-kubernetes/). +- A Docker registry where you pushed the `DemoActor` image. + +#### Deploy the Actor service +For quick deployment you can install dapr in dev mode using the following command: + +``` Bash +dapr init -k --dev +``` + +To deploy the `DemoActor` service to Kubernetes, you can use the provided Kubernetes manifest file `demo-actor.yaml` in the `DemoActor` project directory. +Before applying the manifest file, replace the image name in the manifest file with the image name you pushed to your Docker registry. + +Part to update in `demo-actor.yaml`: +``` YAML +image: /demoactor:latest +``` + +To install the application in `default` namespace, run the following command: + +``` Bash +kubectl apply -f demo-actor.yaml +``` + +This will deploy the `DemoActor` service to Kubernetes. You can check the status of the deployment by running: + +``` Bash +kubectl get pods -n default --watch +``` + +The manifest create 2 services: + +- `demoactor` service: The service that hosts the `DemoActor` actor. +- `demoactor-dapr` service: The service that hosts the Dapr sidecar for the `DemoActor` actor. + +### Make client calls to the deployed Actor service +To make client calls to the deployed `DemoActor` service, you can use the `ActorClient` project. +Before running the client, update the `DAPR_HTTP_PORT` environment variable in the `ActorClient` project directory to the port on which Dapr is running in the Kubernetes cluster. + +On Linux, MacOS: +``` Bash +export DAPR_HTTP_PORT=3500 +``` + +Than port-forward the `DemoActor` service to your local machine: + +``` Bash +kubectl port-forward svc/demoactor 3500:3500 +``` + +Now you can run the client project from the `ActorClient` directory: + +``` Bash +dotnet run +``` \ No newline at end of file From 76d2c3eadaaf625dbb11da9ff8196e4e8e3eea0f Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo Date: Wed, 26 Jun 2024 11:24:20 +0200 Subject: [PATCH 63/84] 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 --- .../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 ddce8a2972c4ba4a9f5a7d7c6cd7a22412294d5b Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Wed, 26 Jun 2024 23:37:19 +0200 Subject: [PATCH 64/84] 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 --- src/Dapr.Actors/ActorReference.cs | 30 ++++--- test/Dapr.Actors.Test/ActorReferenceTests.cs | 93 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 test/Dapr.Actors.Test/ActorReferenceTests.cs diff --git a/src/Dapr.Actors/ActorReference.cs b/src/Dapr.Actors/ActorReference.cs index 0e199c3ae..d72b6676f 100644 --- a/src/Dapr.Actors/ActorReference.cs +++ b/src/Dapr.Actors/ActorReference.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ namespace Dapr.Actors using System; using System.Runtime.Serialization; using Dapr.Actors.Client; + using Dapr.Actors.Runtime; /// /// Encapsulation of a reference to an actor for serialization. @@ -69,23 +70,28 @@ public object Bind(Type actorInterfaceType) private static ActorReference GetActorReference(object actor) { - if (actor == null) - { - throw new ArgumentNullException("actor"); - } + ArgumentNullException.ThrowIfNull(actor, nameof(actor)); - // try as IActorProxy for backward compatibility as customers's mock framework may rely on it before V2 remoting stack. - if (actor is IActorProxy actorProxy) + var actorReference = actor switch { - return new ActorReference() + // try as IActorProxy for backward compatibility as customers's mock framework may rely on it before V2 remoting stack. + IActorProxy actorProxy => new ActorReference() { ActorId = actorProxy.ActorId, ActorType = actorProxy.ActorType, - }; - } + }, + // Handle case when we want to get ActorReference inside the Actor implementation, + // we gather actor id and actor type from Actor base class. + Actor actorBase => new ActorReference() + { + ActorId = actorBase.Id, + ActorType = actorBase.Host.ActorTypeInfo.ActorTypeName, + }, + // Handle case when we can't cast to IActorProxy or Actor. + _ => throw new ArgumentOutOfRangeException("actor", "Invalid actor object type."), + }; - // TODO check for ActorBase - throw new ArgumentOutOfRangeException("actor"); + return actorReference; } } } diff --git a/test/Dapr.Actors.Test/ActorReferenceTests.cs b/test/Dapr.Actors.Test/ActorReferenceTests.cs new file mode 100644 index 000000000..7450f616c --- /dev/null +++ b/test/Dapr.Actors.Test/ActorReferenceTests.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading.Tasks; +using Dapr.Actors.Client; +using Dapr.Actors.Runtime; +using Dapr.Actors.Test; +using Xunit; + +namespace Dapr.Actors +{ + public class ActorReferenceTests + { + [Fact] + public void Get_WhenActorIsNull_ReturnsNull() + { + // Arrange + object actor = null; + + // Act + var result = ActorReference.Get(actor); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Get_FromActorProxy_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = "TestActor"; + var proxy = ActorProxy.Create(expectedActorId, typeof(ITestActor), expectedActorType); + + // Act + var actorReference = ActorReference.Get(proxy); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public async Task Get_FromActorImplementation_ReturnsActorReference() + { + // Arrange + var expectedActorId = new ActorId("abc"); + var expectedActorType = nameof(ActorReferenceTestActor); + var host = ActorHost.CreateForTest(new ActorTestOptions() { ActorId = expectedActorId }); + var actor = new ActorReferenceTestActor(host); + + // Act + var actorReference = await actor.GetActorReference(); + + // Assert + Assert.NotNull(actorReference); + Assert.Equal(expectedActorId, actorReference.ActorId); + Assert.Equal(expectedActorType, actorReference.ActorType); + } + + [Fact] + public void Get_WithInvalidObjectType_ThrowArgumentOutOfRangeException() + { + // Arrange + var actor = new object(); + + // Act + var act = () => ActorReference.Get(actor); + + // Assert + var exception = Assert.Throws(act); + Assert.Equal("actor", exception.ParamName); + Assert.Equal("Invalid actor object type. (Parameter 'actor')", exception.Message); + } + } + + public interface IActorReferenceTestActor : IActor + { + Task GetActorReference(); + } + + public class ActorReferenceTestActor : Actor, IActorReferenceTestActor + { + public ActorReferenceTestActor(ActorHost host) + : base(host) + { + } + + public Task GetActorReference() + { + return Task.FromResult(ActorReference.Get(this)); + } + } +} From 3768a983b7de973e22c7f070c315e13c955b6e07 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 3 Jul 2024 12:47:54 -0500 Subject: [PATCH 65/84] 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 --- src/Dapr.Client/DaprClient.cs | 40 ++++++++++-- src/Dapr.Client/DaprClientGrpc.cs | 46 +++++++++++++- .../{ => Extensions}/EnumExtensions.cs | 5 +- src/Dapr.Client/Extensions/HttpExtensions.cs | 51 +++++++++++++++ .../DaprClientTest.InvokeMethodAsync.cs | 40 ++++++++++++ .../{ => Extensions}/EnumExtensionTest.cs | 6 +- .../Extensions/HttpExtensionTest.cs | 63 +++++++++++++++++++ 7 files changed, 238 insertions(+), 13 deletions(-) rename src/Dapr.Client/{ => Extensions}/EnumExtensions.cs (88%) create mode 100644 src/Dapr.Client/Extensions/HttpExtensions.cs rename test/Dapr.Client.Test/{ => Extensions}/EnumExtensionTest.cs (87%) create mode 100644 test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 21777105b..4f89d8668 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -306,6 +306,20 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName); } + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the POST HTTP method. + /// + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, IReadOnlyCollection> queryStringParameters) + { + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, queryStringParameters); + } + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -317,6 +331,19 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName); + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, + string methodName, IReadOnlyCollection> queryStringParameters); + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -329,9 +356,9 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodN /// An for use with SendInvokeMethodRequestAsync. public HttpRequestMessage CreateInvokeMethodRequest(string appId, string methodName, TRequest data) { - return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, data); + return CreateInvokeMethodRequest(HttpMethod.Post, appId, methodName, new List>(), data); } - + /// /// Creates an that can be used to perform service invocation for the /// application identified by and invokes the method specified by @@ -343,9 +370,10 @@ public HttpRequestMessage CreateInvokeMethodRequest(string appId, stri /// The Dapr application id to invoke the method on. /// The name of the method to invoke. /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. /// An for use with SendInvokeMethodRequestAsync. - public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, TRequest data); - + public abstract HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, IReadOnlyCollection> queryStringParameters, TRequest data); + /// /// Perform health-check of Dapr sidecar. Return 'true' if sidecar is healthy. Otherwise 'false'. /// CheckHealthAsync handle and will return 'false' if error will occur on transport level @@ -526,7 +554,7 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } @@ -620,7 +648,7 @@ public Task InvokeMethodAsync( TRequest data, CancellationToken cancellationToken = default) { - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, data); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>(), data); return InvokeMethodAsync(request, cancellationToken); } diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 3cd7de526..af245afc3 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -345,7 +345,32 @@ public override async Task InvokeBindingAsync(BindingRequest re #region InvokeMethod Apis + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// An for use with SendInvokeMethodRequestAsync. public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName) + { + return CreateInvokeMethodRequest(httpMethod, appId, methodName, new List>()); + } + + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by . + /// + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); @@ -356,7 +381,8 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth // // This approach avoids some common pitfalls that could lead to undesired encoding. var path = $"/v1.0/invoke/{appId}/method/{methodName.TrimStart('/')}"; - var request = new HttpRequestMessage(httpMethod, new Uri(this.httpEndpoint, path)); + var requestUri = new Uri(this.httpEndpoint, path).AddQueryParameters(queryStringParameters); + var request = new HttpRequestMessage(httpMethod, requestUri); request.Options.Set(new HttpRequestOptionsKey(AppIdKey), appId); request.Options.Set(new HttpRequestOptionsKey(MethodNameKey), methodName); @@ -369,13 +395,27 @@ public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMeth return request; } - public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, TRequest data) + /// + /// Creates an that can be used to perform service invocation for the + /// application identified by and invokes the method specified by + /// with the HTTP method specified by and a JSON serialized request body specified by + /// . + /// + /// The type of the data that will be JSON serialized and provided as the request body. + /// The to use for the invocation request. + /// The Dapr application id to invoke the method on. + /// The name of the method to invoke. + /// The data that will be JSON serialized and provided as the request body. + /// A collection of key/value pairs to populate the query string from. + /// An for use with SendInvokeMethodRequestAsync. + public override HttpRequestMessage CreateInvokeMethodRequest(HttpMethod httpMethod, string appId, string methodName, + IReadOnlyCollection> queryStringParameters, TRequest data) { ArgumentVerifier.ThrowIfNull(httpMethod, nameof(httpMethod)); ArgumentVerifier.ThrowIfNullOrEmpty(appId, nameof(appId)); ArgumentVerifier.ThrowIfNull(methodName, nameof(methodName)); - var request = CreateInvokeMethodRequest(httpMethod, appId, methodName); + var request = CreateInvokeMethodRequest(httpMethod, appId, methodName, queryStringParameters); request.Content = JsonContent.Create(data, options: this.JsonSerializerOptions); return request; } diff --git a/src/Dapr.Client/EnumExtensions.cs b/src/Dapr.Client/Extensions/EnumExtensions.cs similarity index 88% rename from src/Dapr.Client/EnumExtensions.cs rename to src/Dapr.Client/Extensions/EnumExtensions.cs index 6b058ca77..df9c9ad33 100644 --- a/src/Dapr.Client/EnumExtensions.cs +++ b/src/Dapr.Client/Extensions/EnumExtensions.cs @@ -11,6 +11,7 @@ // limitations under the License. // ------------------------------------------------------------------------ +#nullable enable using System; using System.Reflection; using System.Runtime.Serialization; @@ -27,12 +28,14 @@ internal static class EnumExtensions /// 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(); + return (attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString()) ?? value.ToString(); } } } diff --git a/src/Dapr.Client/Extensions/HttpExtensions.cs b/src/Dapr.Client/Extensions/HttpExtensions.cs new file mode 100644 index 000000000..259d2747d --- /dev/null +++ b/src/Dapr.Client/Extensions/HttpExtensions.cs @@ -0,0 +1,51 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +#nullable enable +using System; +using System.Collections.Generic; +using System.Text; + +namespace Dapr.Client +{ + /// + /// Provides extensions specific to HTTP types. + /// + internal static class HttpExtensions + { + /// + /// Appends key/value pairs to the query string on an HttpRequestMessage. + /// + /// The uri to append the query string parameters to. + /// The key/value pairs to populate the query string with. + public static Uri AddQueryParameters(this Uri? uri, + IReadOnlyCollection>? queryStringParameters) + { + ArgumentNullException.ThrowIfNull(uri, nameof(uri)); + if (queryStringParameters is null) + return uri; + + var uriBuilder = new UriBuilder(uri); + var qsBuilder = new StringBuilder(uriBuilder.Query); + foreach (var kvParam in queryStringParameters) + { + if (qsBuilder.Length > 0) + qsBuilder.Append('&'); + qsBuilder.Append($"{Uri.EscapeDataString(kvParam.Key)}={Uri.EscapeDataString(kvParam.Value)}"); + } + + uriBuilder.Query = qsBuilder.ToString(); + return uriBuilder.Uri; + } + } +} diff --git a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs index 5d46000a1..484f327d0 100644 --- a/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs +++ b/test/Dapr.Client.Test/DaprClientTest.InvokeMethodAsync.cs @@ -518,6 +518,18 @@ public async Task CreateInvokeMethodRequest_TransformsUrlCorrectly(string method Assert.Equal(new Uri(expected).AbsoluteUri, request.RequestUri.AbsoluteUri); } + [Fact] + public async Task CreateInvokeMethodRequest_AppendQueryStringValuesCorrectly() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var request = client.InnerClient.CreateInvokeMethodRequest("test-app", "mymethod", (IReadOnlyCollection>)new List> { new("a", "0"), new("b", "1") }); + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/mymethod?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + } + [Fact] public async Task CreateInvokeMethodRequest_WithoutApiToken_CreatesHttpRequestWithoutApiTokenHeader() { @@ -617,6 +629,34 @@ public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContent() Assert.Equal(data.Color, actual.Color); } + [Fact] + public async Task CreateInvokeMethodRequest_WithData_CreatesJsonContentWithQueryString() + { + await using var client = TestClient.CreateForDaprClient(c => + { + c.UseGrpcEndpoint("http://localhost").UseHttpEndpoint("https://test-endpoint:3501").UseJsonSerializationOptions(this.jsonSerializerOptions); + }); + + var data = new Widget + { + Color = "red", + }; + + var request = client.InnerClient.CreateInvokeMethodRequest(HttpMethod.Post, "test-app", "test", new List> { new("a", "0"), new("b", "1") }, data); + + Assert.Equal(new Uri("https://test-endpoint:3501/v1.0/invoke/test-app/method/test?a=0&b=1").AbsoluteUri, request.RequestUri.AbsoluteUri); + + var content = Assert.IsType(request.Content); + Assert.Equal(typeof(Widget), content.ObjectType); + Assert.Same(data, content.Value); + + // the best way to verify the usage of the correct settings object + var actual = await content.ReadFromJsonAsync(this.jsonSerializerOptions); + Assert.Equal(data.Color, actual.Color); + } + + + [Fact] public async Task InvokeMethodWithResponseAsync_ReturnsMessageWithoutCheckingStatus() { diff --git a/test/Dapr.Client.Test/EnumExtensionTest.cs b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs similarity index 87% rename from test/Dapr.Client.Test/EnumExtensionTest.cs rename to test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs index be78c3861..83c4354f9 100644 --- a/test/Dapr.Client.Test/EnumExtensionTest.cs +++ b/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; using Xunit; -namespace Dapr.Client.Test +namespace Dapr.Client.Test.Extensions { public class EnumExtensionTest { @@ -29,9 +29,9 @@ public void GetValueFromEnumMember_BlueResolvesAsExpected() public enum TestEnum { - [EnumMember(Value="red")] + [EnumMember(Value = "red")] Red, - [EnumMember(Value="YELLOW")] + [EnumMember(Value = "YELLOW")] Yellow, Blue } diff --git a/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs new file mode 100644 index 000000000..7b93c1c91 --- /dev/null +++ b/test/Dapr.Client.Test/Extensions/HttpExtensionTest.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Net.Http; +using Xunit; + +namespace Dapr.Client.Test.Extensions +{ + public class HttpExtensionTest + { + [Fact] + public void AddQueryParameters_ReturnsEmptyQueryStringWithNullParameters() + { + const string uri = "https://localhost/mypath"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_ReturnsOriginalQueryStringWithNullParameters() + { + const string uri = "https://localhost/mypath?a=0&b=1"; + var httpRq = new HttpRequestMessage(HttpMethod.Get, uri); + var updatedUri = httpRq.RequestUri.AddQueryParameters(null); + Assert.Equal(uri, updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildsQueryString() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath?a=0"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "value") + }); + Assert.Equal("https://localhost/mypath?a=0&test=value", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_BuildQueryStringWithDuplicateKeys() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("test", "1"), + new("test", "2"), + new("test", "3") + }); + Assert.Equal("https://localhost/mypath?test=1&test=2&test=3", updatedUri.AbsoluteUri); + } + + [Fact] + public void AddQueryParameters_EscapeSpacesInValues() + { + var httpRq = new HttpRequestMessage(HttpMethod.Get, "https://localhost/mypath"); + var updatedUri = httpRq.RequestUri.AddQueryParameters(new List> + { + new("name1", "John Doe"), + new("name2", "Jane Doe") + }); + Assert.Equal("https://localhost/mypath?name1=John%20Doe&name2=Jane%20Doe", updatedUri.AbsoluteUri); + } + } +} From 56367963f46257fbcb109f671ac78dc445435012 Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:26:24 -0400 Subject: [PATCH 66/84] fix (#1329) Signed-off-by: Hannah Hunter --- daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index f608cd07d..09aed4c8f 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -56,7 +56,7 @@ Console.WriteLine("Returned: id:{0} | Balance:{1}", account.Id, account.Balance) #### gRPC You can use the `DaprClient` to invoke your services over gRPC. -{{% codetab %}} + ```csharp using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); var invoker = DaprClient.CreateInvocationInvoker(appId: myAppId, daprEndpoint: serviceEndpoint); @@ -67,8 +67,6 @@ await client.MyMethodAsync(new Empty(), options); Assert.Equal(StatusCode.DeadlineExceeded, ex.StatusCode); ``` -{{% /codetab %}} - - For a full guide on service invocation visit [How-To: Invoke a service]({{< ref howto-invoke-discover-services.md >}}). @@ -162,7 +160,7 @@ var secrets = await client.GetSecretAsync("mysecretstore", "key-value-pair-secre Console.WriteLine($"Got secret keys: {string.Join(", ", secrets.Keys)}"); ``` -{{% / codetab %}} +{{% /codetab %}} {{% codetab %}} From b8e276728935c66b0a335b5aa2ca4102c560dd3d Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Fri, 9 Aug 2024 00:37:27 -0400 Subject: [PATCH 67/84] link to non-dapr endpoint howto (#1335) Signed-off-by: Hannah Hunter --- daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md index 09aed4c8f..cab44468b 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -24,6 +24,12 @@ The .NET SDK allows you to interface with all of the [Dapr building blocks]({{< #### HTTP You can either use the `DaprClient` or `System.Net.Http.HttpClient` to invoke your services. +{{% alert title="Note" color="primary" %}} + You can also [invoke a non-Dapr endpoint using either a named `HTTPEndpoint` or an FQDN URL to the non-Dapr environment]({{< ref "howto-invoke-non-dapr-endpoints.md#using-an-httpendpoint-resource-or-fqdn-url-for-non-dapr-endpoints" >}}). + +{{% /alert %}} + + {{< tabs SDK HTTP>}} {{% codetab %}} From 74f6b0127fb33d72ff1c62aecfce8b1694a6b334 Mon Sep 17 00:00:00 2001 From: Phillip Hoff Date: Wed, 21 Aug 2024 23:47:39 -0700 Subject: [PATCH 68/84] Merge 1.14 release branch back into `master`. (#1337) --- .devcontainer/localinit.sh | 2 +- .github/workflows/itests.yml | 6 +- daprdocs/content/en/dotnet-sdk-docs/_index.md | 2 +- .../dotnet-actors/dotnet-actors-howto.md | 2 +- examples/Actor/README.md | 4 +- .../BulkPublishEventExample/README.md | 2 +- .../PublishEventExample/README.md | 2 +- samples/Client/README.md | 2 +- .../Protos/dapr/proto/common/v1/common.proto | 2 +- .../dapr/proto/dapr/v1/appcallback.proto | 32 ++- .../Protos/dapr/proto/dapr/v1/dapr.proto | 187 ++++++++++++++++-- 11 files changed, 212 insertions(+), 31 deletions(-) diff --git a/.devcontainer/localinit.sh b/.devcontainer/localinit.sh index 80b27e4f4..69c4c1274 100644 --- a/.devcontainer/localinit.sh +++ b/.devcontainer/localinit.sh @@ -6,4 +6,4 @@ az extension add --name containerapp --yes nvm install v18.12.1 # initialize Dapr -dapr init --runtime-version=1.10.0-rc.2 \ No newline at end of file +dapr init --runtime-version=1.14.0 \ No newline at end of file diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index 00121c9f5..36741ce7c 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -42,9 +42,9 @@ jobs: GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.13.0 - DAPR_RUNTIME_VER: 1.13.2 - DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.12/install/install.sh + DAPR_CLI_VER: 1.14.0 + DAPR_RUNTIME_VER: 1.14.0 + DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh DAPR_CLI_REF: '' steps: - name: Set up Dapr CLI diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index e823ca29f..121dde310 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -18,7 +18,7 @@ Dapr offers a variety of packages to help with the development of .NET applicati - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}) -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed ## Installation diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md index 8229d6820..eaa13625d 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-actors/dotnet-actors-howto.md @@ -45,7 +45,7 @@ This project contains the implementation of the actor client which calls MyActor - [Dapr CLI]({{< ref install-dapr-cli.md >}}) installed. - Initialized [Dapr environment]({{< ref install-dapr-selfhost.md >}}). -- [.NET Core 3.1 or .NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). +- [.NET 6+](https://dotnet.microsoft.com/download) installed. Dapr .NET SDK uses [ASP.NET Core](https://docs.microsoft.com/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0). ## Step 0: Prepare diff --git a/examples/Actor/README.md b/examples/Actor/README.md index a7bb46c03..89b6bf0bb 100644 --- a/examples/Actor/README.md +++ b/examples/Actor/README.md @@ -4,7 +4,7 @@ The Actor example shows how to create a virtual actor (`DemoActor`) and invoke i ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) @@ -102,7 +102,7 @@ docker push /demo-actor:latest ### Deploy the Actor service to Kubernetes #### Prerequisites - A Kubernetes cluster with `kubectl` configured to access it. -- Dapr v1.13+ installed on the Kubernetes cluster. Follow the instructions [here](https://docs.dapr.io/getting-started/install-dapr-kubernetes/). +- Dapr v1.14+ installed on the Kubernetes cluster. Follow the instructions [here](https://docs.dapr.io/getting-started/install-dapr-kubernetes/). - A Docker registry where you pushed the `DemoActor` image. #### Deploy the Actor service diff --git a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md index dfcc99ca6..39d206fa2 100644 --- a/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/BulkPublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/examples/Client/PublishSubscribe/PublishEventExample/README.md b/examples/Client/PublishSubscribe/PublishEventExample/README.md index 455fc2537..9f3af565f 100644 --- a/examples/Client/PublishSubscribe/PublishEventExample/README.md +++ b/examples/Client/PublishSubscribe/PublishEventExample/README.md @@ -2,7 +2,7 @@ ## Prerequisites -- [.NET Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) installed +- [.NET 6+](https://dotnet.microsoft.com/download) installed - [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) diff --git a/samples/Client/README.md b/samples/Client/README.md index 2501bfadc..2bb738d89 100644 --- a/samples/Client/README.md +++ b/samples/Client/README.md @@ -8,7 +8,7 @@ The following examples will show you how to: ## Prerequisites -* [.Net Core 3.1 or .NET 5+](https://dotnet.microsoft.com/download) +* [.NET 6+](https://dotnet.microsoft.com/download) * [Dapr CLI](https://github.com/dapr/cli) * [Dapr DotNet SDK](https://github.com/dapr/dotnet-sdk) diff --git a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto index 1e63b885d..4acf9159d 100644 --- a/src/Dapr.Client/Protos/dapr/proto/common/v1/common.proto +++ b/src/Dapr.Client/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.Client/Protos/dapr/proto/dapr/v1/appcallback.proto b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto index 823c0aae4..a86040364 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto +++ b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/appcallback.proto @@ -15,6 +15,7 @@ syntax = "proto3"; package dapr.proto.runtime.v1; +import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "dapr/proto/common/v1/common.proto"; import "google/protobuf/struct.proto"; @@ -59,8 +60,37 @@ service AppCallbackHealthCheck { service AppCallbackAlpha { // Subscribes bulk events from Pubsub rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} + + // Sends job back to the app's endpoint at trigger time. + rpc OnJobEventAlpha1 (JobEventRequest) returns (JobEventResponse); +} + +message JobEventRequest { + // Job name. + string name = 1; + + // Job data to be sent back to app. + google.protobuf.Any data = 2; + + // Required. method is a method name which will be invoked by caller. + string method = 3; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 4; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + common.v1.HTTPExtension http_extension = 5; } +// JobEventResponse is the response from the app when a job is triggered. +message JobEventResponse {} + // TopicEventRequest message is compatible with CloudEvent spec v1.0 // https://github.com/cloudevents/spec/blob/v1.0/spec.md message TopicEventRequest { @@ -310,4 +340,4 @@ message ListInputBindingsResponse { // HealthCheckResponse is the message with the response to the health check. // This message is currently empty as used as placeholder. -message HealthCheckResponse {} +message HealthCheckResponse {} \ No newline at end of file diff --git a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto index 5ec1cc9d8..4185fb391 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto @@ -19,6 +19,7 @@ import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; import "dapr/proto/common/v1/common.proto"; +import "dapr/proto/dapr/v1/appcallback.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; option java_outer_classname = "DaprProtos"; @@ -58,6 +59,10 @@ service Dapr { // Bulk Publishes multiple events to the specified topic. rpc BulkPublishEventAlpha1(BulkPublishRequest) returns (BulkPublishResponse) {} + // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic + // events from it. + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream TopicEventRequest) {} + // Invokes binding data to specific output bindings rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} @@ -188,6 +193,15 @@ service Dapr { rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} // Shutdown the sidecar rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} + + // Create and schedule a job + rpc ScheduleJobAlpha1(ScheduleJobRequest) returns (ScheduleJobResponse) {} + + // Gets a scheduled job + rpc GetJobAlpha1(GetJobRequest) returns (GetJobResponse) {} + + // Delete a job + rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} } // InvokeServiceRequest represents the request message for Service invocation. @@ -411,6 +425,47 @@ message BulkPublishResponseFailedEntry { string error = 2; } +// SubscribeTopicEventsRequestAlpha1 is a message containing the details for +// subscribing to a topic via streaming. +// The first message must always be the initial request. All subsequent +// messages must be event responses. +message SubscribeTopicEventsRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeTopicEventsInitialRequestAlpha1 initial_request = 1; + SubscribeTopicEventsResponseAlpha1 event_response = 2; + } +} + +// SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the +// details for subscribing to a topic via streaming. +message SubscribeTopicEventsInitialRequestAlpha1 { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 3; + + // dead_letter_topic is the topic to which messages that fail to be processed + // are sent. + optional string dead_letter_topic = 4; +} + +// SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +// subscription to a topic. +message SubscribeTopicEventsResponseAlpha1 { + // id is the unique identifier for the subscription request. + string id = 1; + + // status is the result of the subscription request. + TopicEventResponse status = 2; +} + // InvokeBindingRequest is the message to send data to output bindings message InvokeBindingRequest { // The name of the output binding to invoke. @@ -504,45 +559,45 @@ message ExecuteStateTransactionRequest { // RegisterActorTimerRequest is the message to register a timer for an actor of a given type and id. message RegisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; string callback = 6; - bytes data = 7; + bytes data = 7; string ttl = 8; } // UnregisterActorTimerRequest is the message to unregister an actor timer message UnregisterActorTimerRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } // RegisterActorReminderRequest is the message to register a reminder for an actor of a given type and id. message RegisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; - string due_time = 4; + string due_time = 4 [json_name = "dueTime"]; string period = 5; - bytes data = 6; + bytes data = 6; string ttl = 7; } // UnregisterActorReminderRequest is the message to unregister an actor reminder. message UnregisterActorReminderRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string name = 3; } // GetActorStateRequest is the message to get key-value states from specific actor. message GetActorStateRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string key = 3; } @@ -556,8 +611,8 @@ message GetActorStateResponse { // ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. message ExecuteActorStateTransactionRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; repeated TransactionalActorStateOperation operations = 3; } @@ -575,8 +630,8 @@ message TransactionalActorStateOperation { // InvokeActorRequest is the message to call an actor. message InvokeActorRequest { - string actor_type = 1; - string actor_id = 2; + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; string method = 3; bytes data = 4; map metadata = 5; @@ -605,6 +660,7 @@ message GetMetadataResponse { string runtime_version = 8 [json_name = "runtimeVersion"]; repeated string enabled_features = 9 [json_name = "enabledFeatures"]; ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; + //TODO: Cassie: probably add scheduler runtime status } message ActorRuntime { @@ -665,6 +721,19 @@ message PubsubSubscription { map metadata = 3 [json_name = "metadata"]; PubsubSubscriptionRules rules = 4 [json_name = "rules"]; string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; + PubsubSubscriptionType type = 6 [json_name = "type"]; +} + +// PubsubSubscriptionType indicates the type of subscription +enum PubsubSubscriptionType { + // UNKNOWN is the default value for the subscription type. + UNKNOWN = 0; + // Declarative subscription (k8s CRD) + DECLARATIVE = 1; + // Programmatically created subscription + PROGRAMMATIC = 2; + // Bidirectional Streaming subscription + STREAMING = 3; } message PubsubSubscriptionRules { @@ -1108,3 +1177,85 @@ message PurgeWorkflowRequest { message ShutdownRequest { // Empty } + +// Job is the definition of a job. At least one of schedule or due_time must be +// provided but can also be provided together. +message Job { + // The unique name for the job. + string name = 1 [json_name = "name"]; + + // schedule is an optional schedule at which the job is to be run. + // Accepts both systemd timer style cron expressions, as well as human + // readable '@' prefixed period strings as defined below. + // + // Systemd timer style cron accepts 6 fields: + // seconds | minutes | hours | day of month | month | day of week + // 0-59 | 0-59 | 0-23 | 1-31 | 1-12/jan-dec | 0-7/sun-sat + // + // "0 30 * * * *" - every hour on the half hour + // "0 15 3 * * *" - every day at 03:15 + // + // Period string expressions: + // Entry | Description | Equivalent To + // ----- | ----------- | ------------- + // @every | Run every (e.g. '@every 1h30m') | N/A + // @yearly (or @annually) | Run once a year, midnight, Jan. 1st | 0 0 0 1 1 * + // @monthly | Run once a month, midnight, first of month | 0 0 0 1 * * + // @weekly | Run once a week, midnight on Sunday | 0 0 0 * * 0 + // @daily (or @midnight) | Run once a day, midnight | 0 0 0 * * * + // @hourly | Run once an hour, beginning of hour | 0 0 * * * * + optional string schedule = 2 [json_name = "schedule"]; + + // repeats is the optional number of times in which the job should be + // triggered. If not set, the job will run indefinitely or until expiration. + optional uint32 repeats = 3 [json_name = "repeats"]; + + // due_time is the optional time at which the job should be active, or the + // "one shot" time if other scheduling type fields are not provided. Accepts + // a "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string due_time = 4 [json_name = "dueTime"]; + + // ttl is the optional time to live or expiration of the job. Accepts a + // "point in time" string in the format of RFC3339, Go duration string + // (calculated from job creation time), or non-repeating ISO8601. + optional string ttl = 5 [json_name = "ttl"]; + + // payload is the serialized job payload that will be sent to the recipient + // when the job is triggered. + google.protobuf.Any data = 6 [json_name = "data"]; +} + +// ScheduleJobRequest is the message to create/schedule the job. +message ScheduleJobRequest { + // The job details. + Job job = 1; +} + +// ScheduleJobResponse is the message response to create/schedule the job. +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message to retrieve a job. +message GetJobRequest { + // The name of the job. + string name = 1; +} + +// GetJobResponse is the message's response for a job retrieved. +message GetJobResponse { + // The job details. + Job job = 1; +} + +// DeleteJobRequest is the message to delete the job by name. +message DeleteJobRequest { + // The name of the job. + string name = 1; +} + +// DeleteJobResponse is the message response to delete the job by name. +message DeleteJobResponse { + // Empty +} From 0709a586f9a89a91bdeed382b2713b21bacd4e8d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 3 Sep 2024 06:30:41 -0500 Subject: [PATCH 69/84] Removed deprecated methods from DaprClient and tests as well as unused types Signed-off-by: Whit Waldo --- 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 | 91 +----- 28 files changed, 53 insertions(+), 739 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 af245afc3..73ab1ec67 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 d95929ca3..cfcbc4bd8 100644 --- a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -19,7 +19,7 @@ using FluentAssertions; using Xunit; using System.Linq; -using System.Diagnostics; +using Dapr.Workflow; namespace Dapr.E2E.Test { @@ -79,94 +79,5 @@ public async Task TestWorkflowLogging() Assert.True(false, "The logs were not able to found within the timeout"); } } - [Fact] - public async Task TestWorkflows() - { - var instanceId = "testInstanceId"; - var instanceId2 = "EventRaiseId"; - var workflowComponent = "dapr"; - var workflowName = "PlaceOrder"; - object input = "paperclips"; - Dictionary workflowOptions = new Dictionary(); - workflowOptions.Add("task_queue", "testQueue"); - - using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); - var health = await daprClient.CheckHealthAsync(); - health.Should().Be(true, "DaprClient is not healthy"); - - // START WORKFLOW TEST - var startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions); - - startResponse.InstanceId.Should().Be("testInstanceId", $"Instance ID {startResponse.InstanceId} was not correct"); - - // GET INFO TEST - var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.InstanceId.Should().Be("testInstanceId"); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PAUSE TEST: - await daprClient.PauseWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Suspended, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RESUME TEST: - await daprClient.ResumeWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Running, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // RAISE EVENT TEST - await daprClient.RaiseWorkflowEventAsync(instanceId, workflowComponent, "ChangePurchaseItem", "computers"); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - - // TERMINATE TEST: - await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent); - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - getResponse.RuntimeStatus.Should().Be(WorkflowRuntimeStatus.Terminated, $"Instance ID {getResponse.RuntimeStatus} was not correct"); - - // PURGE TEST - await daprClient.PurgeWorkflowAsync(instanceId, workflowComponent); - - try - { - getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent); - Assert.True(false, "The GetWorkflowAsync call should have failed since the instance was purged"); - } - catch (DaprException ex) - { - ex.InnerException.Message.Should().Contain("no such instance exists", $"Instance {instanceId} was not correctly purged"); - } - - // Start another workflow for event raising purposes - startResponse = await daprClient.StartWorkflowAsync( - instanceId: instanceId2, - workflowComponent: workflowComponent, - workflowName: workflowName, - input: input, - workflowOptions: workflowOptions); - - // PARALLEL RAISE EVENT TEST - var event1 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event2 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event3 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event4 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - var event5 = daprClient.RaiseWorkflowEventAsync(instanceId2, workflowComponent, "ChangePurchaseItem", "computers"); - - var externalEvents = Task.WhenAll(event1, event2, event3, event4, event5); - var winner = await Task.WhenAny(externalEvents, Task.Delay(TimeSpan.FromSeconds(30))); - externalEvents.IsCompletedSuccessfully.Should().BeTrue($"Unsuccessful at raising events. Status of events: {externalEvents.IsCompletedSuccessfully}"); - - // Wait up to 30 seconds for the workflow to complete and check the output - using var cts = new CancellationTokenSource(delay: TimeSpan.FromSeconds(30)); - getResponse = await daprClient.WaitForWorkflowCompletionAsync(instanceId2, workflowComponent, cts.Token); - var outputString = getResponse.Properties["dapr.workflow.output"]; - outputString.Should().Be("\"computers\"", $"Purchased item {outputString} was not correct"); - var deserializedOutput = getResponse.ReadOutputAs(); - deserializedOutput.Should().Be("computers", $"Deserialized output '{deserializedOutput}' was not expected"); - } } } From 366a3b390e2e52387c1e0e828078663ee6ae63a7 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 3 Sep 2024 18:45:36 -0500 Subject: [PATCH 70/84] Removed unused (and invalid) reference Signed-off-by: Whit Waldo --- 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 167c5226a0796b38308bb3f6fd49e2716fae5daf Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 13 Sep 2024 00:38:41 -0500 Subject: [PATCH 71/84] Added unit test to prove out concern raised on Discord Signed-off-by: Whit Waldo --- 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 3d1fa01d0f52298c880f8d92b08690aa317be270 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 13 Sep 2024 11:45:07 -0500 Subject: [PATCH 72/84] Added missing workflow status branch (#1348) Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowState.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dapr.Workflow/WorkflowState.cs b/src/Dapr.Workflow/WorkflowState.cs index aefe4be6b..ea1ffae22 100644 --- a/src/Dapr.Workflow/WorkflowState.cs +++ b/src/Dapr.Workflow/WorkflowState.cs @@ -84,6 +84,8 @@ public WorkflowRuntimeStatus RuntimeStatus return WorkflowRuntimeStatus.Terminated; case OrchestrationRuntimeStatus.Pending: return WorkflowRuntimeStatus.Pending; + case OrchestrationRuntimeStatus.Suspended: + return WorkflowRuntimeStatus.Suspended; default: return WorkflowRuntimeStatus.Unknown; } From 70d092ea79461835848d7b8b04fe86cc3ac7bdd8 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Sep 2024 08:28:04 -0500 Subject: [PATCH 73/84] Removed unused using Signed-off-by: Whit Waldo --- 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 bc62fd5b634ef7c22522f5ba3399135609ea4065 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 6 Oct 2024 02:29:53 -0500 Subject: [PATCH 74/84] Added fix to handle null return values Signed-off-by: Whit Waldo --- 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 23f82fae5f4a819546ffb0567315bac3aa0d3b39 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 7 Oct 2024 10:46:39 -0500 Subject: [PATCH 75/84] Added unit test to validate that headers aren't being stripped off request Signed-off-by: Whit Waldo --- .../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 156ed567cebe31f4a636a3d18113265de9ea02aa Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 7 Oct 2024 10:47:24 -0500 Subject: [PATCH 76/84] Fixed spelling typo Signed-off-by: Whit Waldo --- 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: Fri, 11 Oct 2024 01:47:37 -0500 Subject: [PATCH 77/84] Removed unnecessary null check Signed-off-by: Whit Waldo --- 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 920d7ad80cf0b9b3d5cd05a915d08261a17c7fc3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 11 Oct 2024 02:13:06 -0500 Subject: [PATCH 78/84] 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 --- 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 cfcbc4bd8..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.IO; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Dapr.Client; -using FluentAssertions; -using Xunit; -using System.Linq; -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.True(false, "The logs were not able to found within the timeout"); - } - } - } -} From 2450ced25fb3a6e9e9e8297287b481bdbbc07abd Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Mon, 14 Oct 2024 18:18:36 -0500 Subject: [PATCH 79/84] 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 --- 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 0a978458bbf71e25c22177a69a5ac9abe163ae8b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 15 Oct 2024 18:09:53 -0500 Subject: [PATCH 80/84] 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 --- 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 236567786e06afa7165a720e37f8717bad89357a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 16 Oct 2024 00:21:52 -0500 Subject: [PATCH 81/84] Removes floating classes and introduces Dapr.Common project (#1365) Extracting classes out to common project --------- Signed-off-by: Whit Waldo --- Directory.Packages.props | 1 + all.sln | 7 ++++ .../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, 79 insertions(+), 35 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 228047852..1d5b011ca 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.Common", "src\Dapr.Common\Dapr.Common.csproj", "{B445B19C-A925-4873-8CB7-8317898B6970}" +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 + {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 @@ -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} + {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 aa8b0fd3511fb3d39b2c5cbf67b258d250c9b1d2 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 16 Oct 2024 00:36:20 -0500 Subject: [PATCH 82/84] Extracted Protos out to common project (#1367) Protos pulled out to separate shared project --- 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 23e8df02953c2cf4ad8bc7b5023978f16a92f0e1 Mon Sep 17 00:00:00 2001 From: Shubhdeep Singh Date: Thu, 17 Oct 2024 02:11:46 +0530 Subject: [PATCH 83/84] Improvement of the dotnet-contributing files (#1330) Add link about Dapr bot to contribution documentation --- 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 d5c32f4ecb579e99b184a19c6e727a0eb31d38f6 Mon Sep 17 00:00:00 2001 From: Ilias Date: Thu, 17 Oct 2024 23:39:39 +0100 Subject: [PATCH 84/84] 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 --- .../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() {