From 1bec37d79599af5cfe2202da36d0720ecf3ab09d Mon Sep 17 00:00:00 2001 From: Ryan Lettieri <67934986+RyanLettieri@users.noreply.github.com> Date: Mon, 30 Jan 2023 17:12:00 -0700 Subject: [PATCH 01/17] Workflow Management - Initial Methods (#1003) Initial work for workflows DotNET SDK Signed-off-by: Ryan Lettieri --- .github/workflows/itests.yml | 8 +- src/Dapr.Client/DaprClient.cs | 47 ++++++++ src/Dapr.Client/DaprClientGrpc.cs | 114 ++++++++++++++++++ src/Dapr.Client/GetWorkflowResponse.cs | 26 ++++ .../Protos/dapr/proto/dapr/v1/dapr.proto | 71 ++++++++++- src/Dapr.Client/WorkflowReference.cs | 40 ++++++ src/Dapr.Workflow/Dapr.Workflow.csproj | 2 +- test/Dapr.Client.Test/Dapr.Client.Test.csproj | 2 +- .../Dapr.E2E.Test.App.csproj | 1 + test/Dapr.E2E.Test.App/Startup.cs | 20 +++ test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 3 +- test/Dapr.E2E.Test/Workflows/WorkflowTest.cs | 61 ++++++++++ 12 files changed, 383 insertions(+), 12 deletions(-) create mode 100644 src/Dapr.Client/GetWorkflowResponse.cs create mode 100644 src/Dapr.Client/WorkflowReference.cs create mode 100644 test/Dapr.E2E.Test/Workflows/WorkflowTest.cs diff --git a/.github/workflows/itests.yml b/.github/workflows/itests.yml index b82583bee..b241562bf 100644 --- a/.github/workflows/itests.yml +++ b/.github/workflows/itests.yml @@ -41,15 +41,15 @@ jobs: install-version: '7.0.x' env: NUPKG_OUTDIR: bin/Release/nugets - GOVER: 1.17 + GOVER: 1.19 GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org - DAPR_CLI_VER: 1.8.0 - DAPR_RUNTIME_VER: 1.8.0 + DAPR_CLI_VER: 1.9.1 + DAPR_RUNTIME_VER: 1.9.5 DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/3dacfb672d55f1436c249057aaebbe597e1066f3/install/install.sh DAPR_CLI_REF: '' - DAPR_REF: '' + DAPR_REF: '82e097134cdc7494c5c3be71bf80f7904d6db9ea' steps: - name: Set up Dapr CLI run: wget -q ${{ env.DAPR_INSTALL_URL }} -O - | /bin/bash -s ${{ env.DAPR_CLI_VER }} diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index ef90d5f58..29f0d9b3f 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -975,6 +975,53 @@ public abstract Task Unlock( string lockOwner, CancellationToken cancellationToken = default); + /// + /// 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. + /// The list of options that are potentially needed to start a workflow. + /// The input input for the given workflow. + /// A that can be used to cancel the operation. + /// A containing a + [Obsolete("This API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task StartWorkflowAsync( + string instanceId, + string workflowComponent, + string workflowName, + Object input, + IReadOnlyDictionary workflowOptions = default, + CancellationToken cancellationToken = default); + + /// + /// Attempt to get information about the given workflow. + /// + /// The unique ID of the target workflow instance. + /// The component to interface with. + /// Name of the workflow to run. + /// 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, + string workflowName, + 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); + /// public void Dispose() { diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 28df698d0..b5b0774a8 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -1462,6 +1462,120 @@ public async override Task Unlock( #endregion + + #region Workflow API + /// + [Obsolete] + public async override Task StartWorkflowAsync( + string instanceId, + string workflowComponent, + string workflowName, + Object input, + 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 = JsonSerializer.SerializeToUtf8Bytes(input); + + var request = new Autogenerated.StartWorkflowRequest() + { + InstanceId = instanceId, + WorkflowComponent = workflowComponent, + WorkflowName = workflowName, + Input = ByteString.CopyFrom(jsonUtf8Bytes), + }; + + 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 WorkflowReference(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, + string workflowName, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(instanceId, nameof(instanceId)); + ArgumentVerifier.ThrowIfNullOrEmpty(workflowComponent, nameof(workflowComponent)); + + var request = new Autogenerated.GetWorkflowRequest() + { + InstanceId = instanceId, + WorkflowComponent = workflowComponent, + WorkflowType = workflowName //TODO: Change 'WorkflowType' to 'WorkflowName' once changes go through dapr/dapr + }; + + try + { + var options = CreateCallOptions(headers: null, cancellationToken); + var response = await client.GetWorkflowAlpha1Async(request, options); + var dateTimeValue = new DateTime(response.StartTime, DateTimeKind.Utc); + return new GetWorkflowResponse(response.InstanceId, dateTimeValue, response.Metadata); + } + catch (RpcException ex) + { + throw new DaprException("Get workflow operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + } + + + /// + [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); + } + + } + + #endregion + + #region Dapr Sidecar Methods /// diff --git a/src/Dapr.Client/GetWorkflowResponse.cs b/src/Dapr.Client/GetWorkflowResponse.cs new file mode 100644 index 000000000..408a52592 --- /dev/null +++ b/src/Dapr.Client/GetWorkflowResponse.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.Collections.Generic; + +namespace Dapr.Client +{ + /// + /// Initializes a new . + /// + /// The instance ID assocated with this response. + /// The time at which the workflow started executing. + /// The response metadata. + public record GetWorkflowResponse(string instanceId, DateTime startTime, IReadOnlyDictionary metadata); +} 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 b5bd00db0..52cbfe6f8 100644 --- a/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto +++ b/src/Dapr.Client/Protos/dapr/proto/dapr/v1/dapr.proto @@ -17,6 +17,7 @@ package dapr.proto.runtime.v1; import "google/protobuf/any.proto"; import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; import "dapr/proto/common/v1/common.proto"; option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; @@ -110,6 +111,15 @@ service Dapr { // Sets value in extended metadata of the sidecar rpc SetMetadata (SetMetadataRequest) returns (google.protobuf.Empty) {} + // Start Workflow + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (WorkflowReference) {} + + // Get Workflow details + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Terminate Workflow + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (TerminateWorkflowResponse) {} + // Shutdown the sidecar rpc Shutdown (google.protobuf.Empty) returns (google.protobuf.Empty) {} } @@ -345,10 +355,10 @@ message InvokeBindingRequest { bytes data = 2; // The metadata passing to output binding components - // + // // Common metadata property: - // - ttlInSeconds : the time to live in seconds for the message. - // If set in the binding definition will cause all messages to + // - 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. map metadata = 3; @@ -411,7 +421,7 @@ message TransactionalStateOperation { // The type of operation to be executed string operationType = 1; - // State values to be operated on + // State values to be operated on common.v1.StateItem request = 2; } @@ -504,6 +514,7 @@ message InvokeActorRequest { string actor_id = 2; string method = 3; bytes data = 4; + map metadata = 5; } // InvokeActorResponse is the method that returns an actor invocation response. @@ -517,6 +528,7 @@ message GetMetadataResponse { repeated ActiveActorsCount active_actors_count = 2; repeated RegisteredComponents registered_components = 3; map extended_metadata = 4; + repeated PubsubSubscription subscriptions = 5; } message ActiveActorsCount { @@ -531,6 +543,23 @@ message RegisteredComponents { repeated string capabilities = 4; } +message PubsubSubscription { + string pubsub_name = 1; + string topic = 2; + map metadata = 3; + PubsubSubscriptionRules rules = 4; + string dead_letter_topic = 5; +} + +message PubsubSubscriptionRules { + repeated PubsubSubscriptionRule rules = 1; +} + +message PubsubSubscriptionRule { + string match = 1; + string path = 2; +} + message SetMetadataRequest { string key = 1; string value = 2; @@ -644,4 +673,36 @@ message UnlockResponse { } Status status = 1; -} \ No newline at end of file +} + +message WorkflowReference { + string instance_id = 1; +} + +message GetWorkflowRequest { + string instance_id = 1; + string workflow_type = 2; + string workflow_component = 3; +} + +message GetWorkflowResponse { + string instance_id = 1; + int64 start_time = 2; + map metadata = 3; +} + +message StartWorkflowRequest { + string instance_id = 1; + string workflow_component = 2; + string workflow_name = 3; + map options = 4; + bytes input = 5; +} + +message TerminateWorkflowRequest { + string instance_id = 1; + string workflow_component = 2; +} + +message TerminateWorkflowResponse { +} diff --git a/src/Dapr.Client/WorkflowReference.cs b/src/Dapr.Client/WorkflowReference.cs new file mode 100644 index 000000000..22489bb9d --- /dev/null +++ b/src/Dapr.Client/WorkflowReference.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; + +namespace Dapr.Client +{ + /// + /// Represents the response from invoking a workflow. + /// + public sealed class WorkflowReference + { + /// + /// Initializes a new .` + /// + /// The instance ID assocated with this response. + public WorkflowReference(string instanceId) + { + ArgumentVerifier.ThrowIfNull(instanceId, nameof(instanceId)); + this.InstanceId = instanceId; + } + + /// + /// The instance ID assocated with this workflow. + /// + public string InstanceId { set; get; } + + } +} diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 5c7bf8e98..48a57eb17 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -2,7 +2,7 @@ - net6 + netcoreapp3.1;net6;net7 enable Dapr.Workflow Dapr Workflow Authoring SDK diff --git a/test/Dapr.Client.Test/Dapr.Client.Test.csproj b/test/Dapr.Client.Test/Dapr.Client.Test.csproj index a4f91d7a1..c94f031e9 100644 --- a/test/Dapr.Client.Test/Dapr.Client.Test.csproj +++ b/test/Dapr.Client.Test/Dapr.Client.Test.csproj @@ -9,7 +9,7 @@ all - + 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 ca1004cba..7e114e8df 100644 --- a/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj +++ b/test/Dapr.E2E.Test.App/Dapr.E2E.Test.App.csproj @@ -8,5 +8,6 @@ + diff --git a/test/Dapr.E2E.Test.App/Startup.cs b/test/Dapr.E2E.Test.App/Startup.cs index d6c82413a..14e1358d5 100644 --- a/test/Dapr.E2E.Test.App/Startup.cs +++ b/test/Dapr.E2E.Test.App/Startup.cs @@ -18,6 +18,7 @@ namespace Dapr.E2E.Test using Dapr.E2E.Test.Actors.Timers; using Dapr.E2E.Test.Actors.ExceptionTesting; using Dapr.E2E.Test.App.ErrorTesting; + using Dapr.Workflow; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; @@ -25,6 +26,7 @@ namespace Dapr.E2E.Test using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using System.Threading.Tasks; /// /// Startup class. @@ -54,6 +56,24 @@ public void ConfigureServices(IServiceCollection services) services.AddAuthentication().AddDapr(); services.AddAuthorization(o => o.AddDapr()); services.AddControllers().AddDapr(); + // Register a workflow and associated activity + services.AddDaprWorkflow(options => + { + // Example of registering a "PlaceOrder" workflow function + options.RegisterWorkflow("PlaceOrder", implementation: async (context, input) => + { + // In real life there are other steps related to placing an order, like reserving + // inventory and charging the customer credit card etc. But let's keep it simple ;) + return await context.CallActivityAsync("ShipProduct", "Coffee Beans"); + }); + + // Example of registering a "ShipProduct" workflow activity function + options.RegisterActivity("ShipProduct", implementation: (context, input) => + { + System.Threading.Thread.Sleep(10000); // sleep for 10s to allow the terminate command to come through + return Task.FromResult($"We are shipping {input} to the customer using our hoard of drones!"); + }); + }); services.AddActors(options => { options.Actors.RegisterActor(); diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index a330fbf30..3df269709 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -4,7 +4,7 @@ - + @@ -16,6 +16,7 @@ + diff --git a/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs new file mode 100644 index 000000000..b5421b999 --- /dev/null +++ b/test/Dapr.E2E.Test/Workflows/WorkflowTest.cs @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------ +// Copyright 2022 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.E2E.Test +{ + + using System.Threading; + using System.Threading.Tasks; + using Xunit; + using FluentAssertions; + using System; + using System.Collections.Generic; + using Google.Protobuf; + using Dapr.Client; + + [System.Obsolete] + public partial class E2ETests + { + [Fact] + public async Task TestWorkflows() + { + string instanceId = "TestWorkflowInstanceID"; + string workflowComponent = "dapr"; + string workflowName = "PlaceOrder"; + object input = ByteString.CopyFrom(0x01); + Dictionary workflowOptions = new Dictionary(); + workflowOptions.Add("task_queue", "testQueue"); + CancellationToken cts = new CancellationToken(); + + using var daprClient = new DaprClientBuilder().UseGrpcEndpoint(this.GrpcEndpoint).UseHttpEndpoint(this.HttpEndpoint).Build(); + var health = await daprClient.CheckHealthAsync(); + health.Should().Be(true, "DaprClient is not healthy"); + + // START WORKFLOW TEST + var startResponse = await daprClient.StartWorkflowAsync(instanceId, workflowComponent, workflowName, input, workflowOptions, cts); + startResponse.InstanceId.Should().Be("TestWorkflowInstanceID", $"Instance ID {startResponse.InstanceId} was not correct"); + + // GET INFO TEST + var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, workflowName); + getResponse.instanceId.Should().Be("TestWorkflowInstanceID"); + getResponse.metadata["dapr.workflow.runtime_status"].Should().Be("RUNNING", $"Instance ID {getResponse.metadata["dapr.workflow.runtime_status"]} was not correct"); + + // TERMINATE TEST: + await daprClient.TerminateWorkflowAsync(instanceId, workflowComponent); + getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponent, workflowName); + getResponse.metadata["dapr.workflow.runtime_status"].Should().Be("TERMINATED", $"Instance ID {getResponse.metadata["dapr.workflow.runtime_status"]} was not correct"); + + } + + } +} \ No newline at end of file From 2fb69527c054e11566dd452d52507c44aa2e3e8c Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Wed, 1 Feb 2023 19:05:19 -0700 Subject: [PATCH 02/17] Beefed up the workflows example program and added in statestore functionality Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 14 +++++ .../Activities/ProcessPaymentActivity.cs | 11 ++-- .../Activities/ReserveInventoryActivity.cs | 48 ++++++++++++---- .../Activities/UpdateInventoryActivity.cs | 56 +++++++++++++++++++ examples/Workflow/WorkflowWebApp/Models.cs | 9 +++ examples/Workflow/WorkflowWebApp/Program.cs | 52 ++++++++++++----- .../Workflows/OrderProcessingWorkflow.cs | 37 +++++++++--- 7 files changed, 193 insertions(+), 34 deletions(-) create mode 100644 examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs create mode 100644 examples/Workflow/WorkflowWebApp/Models.cs diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 87cc9b033..b8a63d049 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -25,6 +25,20 @@ dapr run --app-id wfwebapp dotnet run The application will listen for HTTP requests at `http://localhost:10080`. +This workflow example utilizes a redis statestore. In order to retrieve items from this statestore, an HTTP command must first be sent down to restock the inventory: + +On Linux/macOS (bash): + +```bash +curl -i -X POST http://localhost:10080/reset +``` + +On Windows (PowerShell): + +```powershell +curl -i -X POST http://localhost:10080/reset ` +``` + To start a workflow, use the following command to send an HTTP POST request, which triggers an HTTP API that starts the workflow using the Dapr Workflow client. Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. On Linux/macOS (bash): diff --git a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs index 16075e87f..47d1421c9 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs @@ -1,25 +1,28 @@ namespace WorkflowWebApp.Activities { using System.Threading.Tasks; + using Dapr.Client; using Dapr.Workflow; - - record PaymentRequest(string RequestId, double Amount, string Currency); + using WorkflowWebApp.Models; class ProcessPaymentActivity : WorkflowActivity { readonly ILogger logger; + readonly DaprClient client; - public ProcessPaymentActivity(ILoggerFactory loggerFactory) + public ProcessPaymentActivity(ILoggerFactory loggerFactory, DaprClient client) { this.logger = loggerFactory.CreateLogger(); + this.client = client; } public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) { this.logger.LogInformation( - "Processing payment: {requestId}, {amount}, {currency}", + "Processing payment: {requestId} for {amount} {item} at ${currency}", req.RequestId, req.Amount, + req.ItemBeingPruchased, req.Currency); // Simulate slow processing diff --git a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs index 19f57a051..2abef9e24 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs @@ -1,32 +1,60 @@ namespace WorkflowWebApp.Activities { using System.Threading.Tasks; + using Dapr.Client; using Dapr.Workflow; - - record InventoryRequest(string RequestId, string Name, int Quantity); - record InventoryResult(bool Success); + using WorkflowWebApp.Models; class ReserveInventoryActivity : WorkflowActivity { readonly ILogger logger; + readonly DaprClient client; + private static readonly string storeName = "statestore"; - public ReserveInventoryActivity(ILoggerFactory loggerFactory) + public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) { this.logger = loggerFactory.CreateLogger(); + this.client = client; } public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) { this.logger.LogInformation( - "Reserving inventory: {requestId}, {name}, {quantity}", + "Reserving inventory for order {requestId} of {quantity} {name}", req.RequestId, - req.Name, - req.Quantity); + req.Quantity, + req.ItemName); + + OrderPayload original; + string originalETag; + + // Ensure that the store has items + (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemName); + + // Catch for the case where the statestore isn't setup + if (original == null) + { + // Not enough paperclips. + return new InventoryResult(false, original, originalETag); + } + + this.logger.LogInformation( + "There are: {requestId}, {name} available for purchase", + original.Quantity, + original.Name); + + // See if there're enough paperclips to purchase + if (original.Quantity >= req.Quantity) + { + // Simulate slow processing + await Task.Delay(TimeSpan.FromSeconds(2)); + + return new InventoryResult(true, original, originalETag); + } - // Simulate slow processing - await Task.Delay(TimeSpan.FromSeconds(2)); + // Not enough paperclips. + return new InventoryResult(false, original, originalETag); - return new InventoryResult(true); } } } diff --git a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs new file mode 100644 index 000000000..fbdfb4a60 --- /dev/null +++ b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs @@ -0,0 +1,56 @@ +namespace WorkflowWebApp.Activities +{ + using System.Threading.Tasks; + using Dapr.Client; + using Dapr.Workflow; + using WorkflowWebApp.Models; + using DurableTask.Core.Exceptions; + + class UpdateInventoryActivity : WorkflowActivity + { + readonly ILogger logger; + readonly DaprClient client; + private static readonly string storeName = "statestore"; + + public UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) + { + this.logger = loggerFactory.CreateLogger(); + this.client = client; + } + + public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) + { + this.logger.LogInformation( + "Checking Inventory for: Order# {requestId} for {amount} {item}", + req.RequestId, + req.Amount, + req.ItemBeingPruchased); + + // Simulate slow processing + await Task.Delay(TimeSpan.FromSeconds(5)); + + // Determine if there are enough Paperclips for purchase + var (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); + var newQuantity = original.Quantity - req.Amount; + + if (newQuantity < 0) + { + this.logger.LogInformation( + "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", + req.RequestId); + throw new TaskFailedException(); + } + + // Update the statestore with the new amount of paper clips + await client.SaveStateAsync(storeName, req.ItemBeingPruchased, new OrderPayload(Name: req.ItemBeingPruchased, TotalCost: req.Currency, Quantity: newQuantity)); + (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); + this.logger.LogInformation($"There are now: {original.Quantity} {original.Name} left in stock"); + + this.logger.LogInformation( + "OrderID '{requestId}' processed successfully", + req.RequestId); + + return null; + } + } +} diff --git a/examples/Workflow/WorkflowWebApp/Models.cs b/examples/Workflow/WorkflowWebApp/Models.cs new file mode 100644 index 000000000..8e78da516 --- /dev/null +++ b/examples/Workflow/WorkflowWebApp/Models.cs @@ -0,0 +1,9 @@ +namespace WorkflowWebApp.Models +{ + record OrderPayload(string Name, double TotalCost, int Quantity = 1); + record InventoryRequest(string RequestId, string ItemName, int Quantity); + record InventoryResult(bool Success, OrderPayload orderPayload, string etag); + record PaymentRequest(string RequestId, string ItemBeingPruchased, int Amount, double Currency); + record OrderResult(bool Processed); + +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowWebApp/Program.cs b/examples/Workflow/WorkflowWebApp/Program.cs index 119bc2af4..61fccde4d 100644 --- a/examples/Workflow/WorkflowWebApp/Program.cs +++ b/examples/Workflow/WorkflowWebApp/Program.cs @@ -3,7 +3,9 @@ using Microsoft.AspNetCore.Mvc; using WorkflowWebApp.Activities; using WorkflowWebApp.Workflows; +using WorkflowWebApp.Models; using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; +using Dapr.Client; // The workflow host is a background service that connects to the sidecar over gRPC WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -26,13 +28,24 @@ options.RegisterActivity(); options.RegisterActivity(); options.RegisterActivity(); + options.RegisterActivity(); }); WebApplication app = builder.Build(); + // POST starts new order workflow instance -app.MapPost("/orders", async (WorkflowEngineClient client, [FromBody] OrderPayload orderInfo) => +app.MapPost("/orders", [Obsolete] async (DaprClient client, [FromBody] OrderPayload orderInfo) => { + // Generate a unique ID for the workflow + string orderId = Guid.NewGuid().ToString()[..8]; + // All the necessary inputs (with workflow options being optional) + string workflowComponent = "dapr"; + string workflowName = "OrderProcessingWorkflow"; + object input = orderInfo; + Dictionary workflowOptions = new Dictionary(); + CancellationToken cts = new CancellationToken(); + if (orderInfo?.Name == null) { return Results.BadRequest(new @@ -42,31 +55,35 @@ }); } - // Randomly generated order ID that is 8 characters long. - string orderId = Guid.NewGuid().ToString()[..8]; - await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), orderId, orderInfo); - + // Start the workflow + var response = await client.StartWorkflowAsync(orderId, workflowComponent, workflowName, input, workflowOptions, cts); + // Get information on the workflow + var state = await client.GetWorkflowAsync(orderId, workflowComponent, workflowName); + orderId = response.InstanceId; // return an HTTP 202 and a Location header to be used for status query return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }); }); // GET fetches state for order workflow to report status -app.MapGet("/orders/{orderId}", async (string orderId, WorkflowEngineClient client) => +app.MapGet("/orders/{orderId}", [Obsolete] async (string orderId, DaprClient client) => { - WorkflowState state = await client.GetWorkflowStateAsync(orderId, true); - if (!state.Exists) + // WorkflowState state = await client.GetWorkflowStateAsync(orderId, true); + string workflowComponent = "dapr"; + string workflowName = "OrderProcessingWorkflow"; + + var state = await client.GetWorkflowAsync(orderId, workflowComponent, workflowName); + + if (state.instanceId == "") { return Results.NotFound($"No order with ID = '{orderId}' was found."); } var httpResponsePayload = new { - details = state.ReadInputAs(), - status = state.RuntimeStatus.ToString(), - result = state.ReadOutputAs(), + status = state.metadata["dapr.workflow.runtime_status"].ToString(), }; - if (state.IsWorkflowRunning) + if (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING") { // HTTP 202 Accepted - async polling clients should keep polling for status return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }, httpResponsePayload); @@ -78,5 +95,14 @@ } }).WithName("GetOrderInfoEndpoint"); -app.Run(); +app.MapPost("/reset", [Obsolete] async (DaprClient client) => +{ + // Make this into a webAPI rather than throwing this into this activity + // Save a bunch of items in the state store + await client.SaveStateAsync("statestore", "Paperclips", new OrderPayload(Name: "Paperclips", TotalCost: 99.95, Quantity: 100)); + + return Results.Ok(); +}); + +app.Run(); \ No newline at end of file diff --git a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs index 035f8911a..605b6cf8c 100644 --- a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs @@ -2,10 +2,9 @@ { using System.Threading.Tasks; using Dapr.Workflow; + using DurableTask.Core.Exceptions; using WorkflowWebApp.Activities; - - record OrderPayload(string Name, double TotalCost, int Quantity = 1); - record OrderResult(bool Processed); + using WorkflowWebApp.Models; class OrderProcessingWorkflow : Workflow { @@ -13,29 +12,53 @@ public override async Task RunAsync(WorkflowContext context, OrderP { string orderId = context.InstanceId; + // Notify the user that an order has come through await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Received order {orderId} for {order.Name} at {order.TotalCost:c}")); + new Notification($"Received order {orderId} for {order.Name} at ${order.TotalCost}")); string requestId = context.InstanceId; + // 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)); + + // If there is insufficient inventory, fail and let the user know if (!result.Success) { // End the workflow here since we don't have sufficient inventory - context.SetCustomStatus($"Insufficient inventory for {order.Name}"); + await context.CallActivityAsync( + nameof(NotifyActivity), + new Notification($"Insufficient inventory for {order.Name}")); return new OrderResult(Processed: false); } + // There is enough inventory available so the user can purchase the item(s). Process their payment await context.CallActivityAsync( nameof(ProcessPaymentActivity), - new PaymentRequest(RequestId: orderId, order.TotalCost, "USD")); + new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost)); + + 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)); + } + catch (TaskFailedException) + { + // Let them know their payment was processed + await context.CallActivityAsync( + nameof(NotifyActivity), + new Notification($"Order {orderId} Failed! You are now getting a refund")); + return new OrderResult(Processed: false); + } + // Let them know their payment was processed await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Order {orderId} processed successfully!")); + new Notification($"Order {orderId} has completed!")); // End the workflow with a success result return new OrderResult(Processed: true); From eb6c45184bc8b565879193c47f7162b4b200e44f Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Wed, 1 Feb 2023 21:59:47 -0700 Subject: [PATCH 03/17] Addressing a bunch of review comments Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 12 +----- .../Activities/ReserveInventoryActivity.cs | 22 +++++------ .../Activities/UpdateInventoryActivity.cs | 13 ++----- examples/Workflow/WorkflowWebApp/Models.cs | 4 +- examples/Workflow/WorkflowWebApp/Program.cs | 39 +++++++------------ .../WorkflowWebApp/WorkflowWebApp.csproj | 1 + 6 files changed, 34 insertions(+), 57 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index b8a63d049..5ab83dc32 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -25,19 +25,9 @@ dapr run --app-id wfwebapp dotnet run The application will listen for HTTP requests at `http://localhost:10080`. -This workflow example utilizes a redis statestore. In order to retrieve items from this statestore, an HTTP command must first be sent down to restock the inventory: +This workflow example utilizes a redis statestore. In order to populate items into the state store, an HTTP command must first be sent down to restock the inventory: -On Linux/macOS (bash): - -```bash curl -i -X POST http://localhost:10080/reset -``` - -On Windows (PowerShell): - -```powershell -curl -i -X POST http://localhost:10080/reset ` -``` To start a workflow, use the following command to send an HTTP POST request, which triggers an HTTP API that starts the workflow using the Dapr Workflow client. Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. diff --git a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs index 2abef9e24..66a3136d8 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs @@ -9,7 +9,7 @@ class ReserveInventoryActivity : WorkflowActivity RunAsync(WorkflowActivityContext con req.Quantity, req.ItemName); - OrderPayload original; - string originalETag; + OrderPayload orderResponse; + string key; // Ensure that the store has items - (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemName); + (orderResponse, key) = await client.GetStateAndETagAsync(storeName, req.ItemName); // Catch for the case where the statestore isn't setup - if (original == null) + if (orderResponse == null) { // Not enough paperclips. - return new InventoryResult(false, original, originalETag); + return new InventoryResult(false, orderResponse); } this.logger.LogInformation( "There are: {requestId}, {name} available for purchase", - original.Quantity, - original.Name); + orderResponse.Quantity, + orderResponse.Name); // See if there're enough paperclips to purchase - if (original.Quantity >= req.Quantity) + if (orderResponse.Quantity >= req.Quantity) { // Simulate slow processing await Task.Delay(TimeSpan.FromSeconds(2)); - return new InventoryResult(true, original, originalETag); + return new InventoryResult(true, orderResponse); } // Not enough paperclips. - return new InventoryResult(false, original, originalETag); + return new InventoryResult(false, orderResponse); } } diff --git a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs index fbdfb4a60..b2aedbced 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs @@ -8,9 +8,9 @@ namespace WorkflowWebApp.Activities class UpdateInventoryActivity : WorkflowActivity { + static readonly string storeName = "statestore"; readonly ILogger logger; readonly DaprClient client; - private static readonly string storeName = "statestore"; public UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) { @@ -31,24 +31,19 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay // Determine if there are enough Paperclips for purchase var (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); - var newQuantity = original.Quantity - req.Amount; + int newQuantity = original.Quantity - req.Amount; if (newQuantity < 0) { this.logger.LogInformation( "Payment for request ID '{requestId}' could not be processed. Insufficient inventory.", req.RequestId); - throw new TaskFailedException(); + throw new InvalidOperationException(); } // Update the statestore with the new amount of paper clips await client.SaveStateAsync(storeName, req.ItemBeingPruchased, new OrderPayload(Name: req.ItemBeingPruchased, TotalCost: req.Currency, Quantity: newQuantity)); - (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); - this.logger.LogInformation($"There are now: {original.Quantity} {original.Name} left in stock"); - - this.logger.LogInformation( - "OrderID '{requestId}' processed successfully", - req.RequestId); + this.logger.LogInformation($"There are now: {newQuantity} {original.Name} left in stock"); return null; } diff --git a/examples/Workflow/WorkflowWebApp/Models.cs b/examples/Workflow/WorkflowWebApp/Models.cs index 8e78da516..c36147a79 100644 --- a/examples/Workflow/WorkflowWebApp/Models.cs +++ b/examples/Workflow/WorkflowWebApp/Models.cs @@ -2,8 +2,8 @@ namespace WorkflowWebApp.Models { record OrderPayload(string Name, double TotalCost, int Quantity = 1); record InventoryRequest(string RequestId, string ItemName, int Quantity); - record InventoryResult(bool Success, OrderPayload orderPayload, string etag); + record InventoryResult(bool Success, OrderPayload orderPayload); record PaymentRequest(string RequestId, string ItemBeingPruchased, int Amount, double Currency); record OrderResult(bool Processed); - + record InventoryItem(string Name, double TotalCost, int Quantity); } \ No newline at end of file diff --git a/examples/Workflow/WorkflowWebApp/Program.cs b/examples/Workflow/WorkflowWebApp/Program.cs index 61fccde4d..b6f50ec01 100644 --- a/examples/Workflow/WorkflowWebApp/Program.cs +++ b/examples/Workflow/WorkflowWebApp/Program.cs @@ -1,11 +1,15 @@ -using System.Text.Json.Serialization; +using Dapr.Client; using Dapr.Workflow; +using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; using Microsoft.AspNetCore.Mvc; +using System.Text.Json.Serialization; using WorkflowWebApp.Activities; -using WorkflowWebApp.Workflows; using WorkflowWebApp.Models; -using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; -using Dapr.Client; +using WorkflowWebApp.Workflows; + + +const string workflowComponent = "dapr"; +const string workflowName = nameof(OrderProcessingWorkflow); // The workflow host is a background service that connects to the sidecar over gRPC WebApplicationBuilder builder = WebApplication.CreateBuilder(args); @@ -35,16 +39,10 @@ // POST starts new order workflow instance -app.MapPost("/orders", [Obsolete] async (DaprClient client, [FromBody] OrderPayload orderInfo) => +app.MapPost("/orders", async (DaprClient client, [FromBody] OrderPayload orderInfo) => { // Generate a unique ID for the workflow string orderId = Guid.NewGuid().ToString()[..8]; - // All the necessary inputs (with workflow options being optional) - string workflowComponent = "dapr"; - string workflowName = "OrderProcessingWorkflow"; - object input = orderInfo; - Dictionary workflowOptions = new Dictionary(); - CancellationToken cts = new CancellationToken(); if (orderInfo?.Name == null) { @@ -56,24 +54,18 @@ } // Start the workflow - var response = await client.StartWorkflowAsync(orderId, workflowComponent, workflowName, input, workflowOptions, cts); - // Get information on the workflow - var state = await client.GetWorkflowAsync(orderId, workflowComponent, workflowName); - orderId = response.InstanceId; + var response = await client.StartWorkflowAsync(orderId, workflowComponent, workflowName, orderInfo, null, CancellationToken.None); + // return an HTTP 202 and a Location header to be used for status query return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }); }); // GET fetches state for order workflow to report status -app.MapGet("/orders/{orderId}", [Obsolete] async (string orderId, DaprClient client) => +app.MapGet("/orders/{orderId}", async (string orderId, DaprClient client) => { - // WorkflowState state = await client.GetWorkflowStateAsync(orderId, true); - string workflowComponent = "dapr"; - string workflowName = "OrderProcessingWorkflow"; - var state = await client.GetWorkflowAsync(orderId, workflowComponent, workflowName); - if (state.instanceId == "") + if (string.IsNullOrEmpty(state.instanceId)) { return Results.NotFound($"No order with ID = '{orderId}' was found."); } @@ -83,7 +75,7 @@ status = state.metadata["dapr.workflow.runtime_status"].ToString(), }; - if (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING") + if (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING" || state.metadata["dapr.workflow.runtime_status"].ToString() == "PENDING") { // HTTP 202 Accepted - async polling clients should keep polling for status return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }, httpResponsePayload); @@ -98,9 +90,8 @@ app.MapPost("/reset", [Obsolete] async (DaprClient client) => { - // Make this into a webAPI rather than throwing this into this activity // Save a bunch of items in the state store - await client.SaveStateAsync("statestore", "Paperclips", new OrderPayload(Name: "Paperclips", TotalCost: 99.95, Quantity: 100)); + await client.SaveStateAsync("statestore", "Paperclips", new InventoryItem(Name: "Paperclips", TotalCost: 99.95, Quantity: 100)); return Results.Ok(); }); diff --git a/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj b/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj index 92a2999d3..23f4f1f7d 100644 --- a/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj +++ b/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj @@ -9,6 +9,7 @@ net6 enable latest + 612,618 From fa864f71352de41677d8cdc93ece2f675b57527a Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Thu, 2 Feb 2023 09:21:01 -0700 Subject: [PATCH 04/17] Updates to readme and demo for workflows Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 14 ++++++++++++-- examples/Workflow/WorkflowWebApp/demo.http | 12 ++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 5ab83dc32..c04663ce9 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -20,14 +20,24 @@ The main `Program.cs` file contains the main setup of the app, including the reg To run the workflow web app locally, run this command in the `WorkflowWebApp` directory: ```sh -dapr run --app-id wfwebapp dotnet run +dapr run --app-id wfwebapp --dapr-http-port 3500 -- dotnet run ``` The application will listen for HTTP requests at `http://localhost:10080`. -This workflow example utilizes a redis statestore. In order to populate items into the state store, an HTTP command must first be sent down to restock the inventory: +This workflow example utilizes a redis statestore. In order to retrieve items from this statestore, an HTTP command must first be sent down to restock the inventory: +On Linux/macOS (bash): + +```bash curl -i -X POST http://localhost:10080/reset +``` + +On Windows (PowerShell): + +```powershell +curl -i -X POST http://localhost:10080/reset ` +``` To start a workflow, use the following command to send an HTTP POST request, which triggers an HTTP API that starts the workflow using the Dapr Workflow client. Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. diff --git a/examples/Workflow/WorkflowWebApp/demo.http b/examples/Workflow/WorkflowWebApp/demo.http index 2fa686385..8d9a31859 100644 --- a/examples/Workflow/WorkflowWebApp/demo.http +++ b/examples/Workflow/WorkflowWebApp/demo.http @@ -1,8 +1,16 @@ +### Reset workflow +POST http://localhost:10080/reset +Content-Type: application/json + + ### Create new order POST http://localhost:10080/orders Content-Type: application/json {"name": "Paperclips", "totalCost": 99.95, "quantity": 1} -### Query placeholder -GET http://localhost:10080/orders/XXX \ No newline at end of file +### Query placeholder - replace xxx with id from location when order is creates +GET http://localhost:10080/orders/xxx + +### Query dapr sidecar directly +GET http://localhost:3500/v1.0-alpha1/workflows/dapr/wfwebapp/xxx \ No newline at end of file From 22714efca3542927c87382e9a6bf6953c554676c Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Thu, 2 Feb 2023 15:11:16 -0700 Subject: [PATCH 05/17] Changed webapp to console app Signed-off-by: Ryan Lettieri --- .../Activities/ReserveInventoryActivity.cs | 6 +- .../Activities/UpdateInventoryActivity.cs | 2 +- examples/Workflow/WorkflowWebApp/Program.cs | 94 +++++++++++++++++-- .../Workflows/OrderProcessingWorkflow.cs | 2 +- 4 files changed, 89 insertions(+), 15 deletions(-) diff --git a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs index 66a3136d8..1d523d688 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs @@ -34,7 +34,7 @@ public override async Task RunAsync(WorkflowActivityContext con // Catch for the case where the statestore isn't setup if (orderResponse == null) { - // Not enough paperclips. + // Not enough items. return new InventoryResult(false, orderResponse); } @@ -43,7 +43,7 @@ public override async Task RunAsync(WorkflowActivityContext con orderResponse.Quantity, orderResponse.Name); - // See if there're enough paperclips to purchase + // See if there're enough items to purchase if (orderResponse.Quantity >= req.Quantity) { // Simulate slow processing @@ -52,7 +52,7 @@ public override async Task RunAsync(WorkflowActivityContext con return new InventoryResult(true, orderResponse); } - // Not enough paperclips. + // Not enough items. return new InventoryResult(false, orderResponse); } diff --git a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs index b2aedbced..6766c3392 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs @@ -29,7 +29,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay // Simulate slow processing await Task.Delay(TimeSpan.FromSeconds(5)); - // Determine if there are enough Paperclips for purchase + // Determine if there are enough Items for purchase var (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); int newQuantity = original.Quantity - req.Amount; diff --git a/examples/Workflow/WorkflowWebApp/Program.cs b/examples/Workflow/WorkflowWebApp/Program.cs index b6f50ec01..1c65aa298 100644 --- a/examples/Workflow/WorkflowWebApp/Program.cs +++ b/examples/Workflow/WorkflowWebApp/Program.cs @@ -6,20 +6,14 @@ using WorkflowWebApp.Activities; using WorkflowWebApp.Models; using WorkflowWebApp.Workflows; - +using System.Text.Json; const string workflowComponent = "dapr"; +const string storeName = "statestore"; const string workflowName = nameof(OrderProcessingWorkflow); // The workflow host is a background service that connects to the sidecar over gRPC -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Configure HTTP JSON options. -builder.Services.Configure(options => -{ - options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; -}); +var builder = WebApplication.CreateBuilder(args); // Dapr workflows are registered as part of the service configuration builder.Services.AddDaprWorkflow(options => @@ -96,4 +90,84 @@ return Results.Ok(); }); -app.Run(); \ No newline at end of file +app.Start(); + +// Start the client +string daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); +if (string.IsNullOrEmpty(daprPortStr)) +{ + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "4001"); +} +using var daprClient = new DaprClientBuilder().Build(); + +// Start the console app +while (true) +{ + var health = await daprClient.CheckHealthAsync(); + Console.WriteLine("Welcome to the workflows example!"); + Console.WriteLine("In this example, you will be starting a workflow and obtaining the status."); + Console.WriteLine("First, ensure that dapr is running and connected to the correct port"); + Console.WriteLine("To do this, run the following command in a separate teminal window: dapr run --dapr-grpc-port 4001 --app-id wfwebapp"); + + // tell the user whats already in store + Console.WriteLine("For starters, the store has been stocked with 100 'Cars', 100 'Paperclips' and 100 'Computers':"); + // Populate the store with items + RestockInventory(); + + // Main Loop + string continueChar = "1"; + while (continueChar == "1") + { + // Generate a unique ID for the workflow + string orderId = Guid.NewGuid().ToString()[..8]; + Console.WriteLine("To get started, enter an item you would like to purchase:"); + var itemToPurchase = Console.ReadLine(); + Console.WriteLine("You are going to purchase: {0}", itemToPurchase); + Console.WriteLine("Next, enter the quantity of {0} that you want to purchase: ", itemToPurchase); + var ammountToPurchase = Convert.ToInt32(Console.ReadLine()); + + // Construct the order + OrderPayload orderInfo = new OrderPayload(itemToPurchase, 99.95, ammountToPurchase); + + Console.WriteLine("Before check inventory!"); + + OrderPayload orderResponse; + string key; + // Ensure that the store has items + (orderResponse, key) = await daprClient.GetStateAndETagAsync(storeName, itemToPurchase); + + Console.WriteLine("After check inventory: {0}", orderResponse); + + // Start the workflow + Console.WriteLine("Starting workflow {0} purchasing {1} {2}", orderId, ammountToPurchase, itemToPurchase); + var response = await daprClient.StartWorkflowAsync(orderId, workflowComponent, workflowName, orderInfo, null, CancellationToken.None); + + var state = await daprClient.GetWorkflowAsync(orderId, workflowComponent, workflowName); + Console.WriteLine("Your workflow has started. Here is the status of the workflow: {0}", state); + while (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING") + { + await Task.Delay(TimeSpan.FromSeconds(5)); + Console.WriteLine("Your workflow is in progress. Here is the status of the workflow: {0}", state); + state = await daprClient.GetWorkflowAsync(orderId, workflowComponent, workflowName); + } + Console.WriteLine("Your workflow has completed: {0}", JsonSerializer.Serialize(state)); + Console.WriteLine("To start another workflow, press 1. If you'd like to restock the store, press 2. Otherwise, you can exit the demo with any other character"); + continueChar = Console.ReadLine(); + + if (continueChar == "2") + { + RestockInventory(); + continueChar = "1"; + } + } + + break; +} + +void RestockInventory() +{ + daprClient.SaveStateAsync(storeName, "Paperclips", new OrderPayload(Name: "PaperClips", TotalCost: 5, Quantity: 100)); + daprClient.SaveStateAsync(storeName, "Cars", new OrderPayload(Name: "Cars", TotalCost: 15000, Quantity: 100)); + daprClient.SaveStateAsync(storeName, "Computers", new OrderPayload(Name: "Computers", TotalCost: 500, Quantity: 100)); + return; +} \ No newline at end of file diff --git a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs index 605b6cf8c..18b2b2952 100644 --- a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs @@ -15,7 +15,7 @@ public override async Task RunAsync(WorkflowContext context, OrderP // Notify the user that an order has come through await context.CallActivityAsync( nameof(NotifyActivity), - new Notification($"Received order {orderId} for {order.Name} at ${order.TotalCost}")); + new Notification($"Received order {orderId} for {order.Quantity} {order.Name} at ${order.TotalCost}")); string requestId = context.InstanceId; From da71c37defadf3605674c81c84da255579aab83b Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Sat, 4 Feb 2023 06:43:10 +0900 Subject: [PATCH 06/17] Update DurableTask SDK dependency to get ARM64 compatibility (#1024) * Update DurableTask SDK dependency to get ARM64 compatibility Signed-off-by: Chris Gillum * Fix issue with gRPC address override behavior Signed-off-by: Chris Gillum Signed-off-by: Ryan Lettieri --- src/Dapr.Workflow/Dapr.Workflow.csproj | 4 ++-- src/Dapr.Workflow/WorkflowEngineClient.cs | 7 +++++-- src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs | 6 ++++++ test/Dapr.E2E.Test/Dapr.E2E.Test.csproj | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 48a57eb17..14f590ba6 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/src/Dapr.Workflow/WorkflowEngineClient.cs b/src/Dapr.Workflow/WorkflowEngineClient.cs index 640bacd30..e808483dd 100644 --- a/src/Dapr.Workflow/WorkflowEngineClient.cs +++ b/src/Dapr.Workflow/WorkflowEngineClient.cs @@ -18,10 +18,13 @@ namespace Dapr.Workflow using Microsoft.DurableTask; using Microsoft.DurableTask.Client; - // TODO: This will be replaced by the official Dapr Workflow management client. /// /// Defines client operations for managing Dapr Workflow instances. /// + /// + /// This is an alternative to the general purpose Dapr client. It uses a gRPC connection to send + /// commands directly to the workflow engine, bypassing the Dapr API layer. + /// public sealed class WorkflowEngineClient : IAsyncDisposable { readonly DurableTaskClient innerClient; @@ -70,7 +73,7 @@ public Task ScheduleNewWorkflowAsync( /// public async Task GetWorkflowStateAsync(string instanceId, bool getInputsAndOutputs = false) { - OrchestrationMetadata? metadata = await this.innerClient.GetInstanceMetadataAsync( + OrchestrationMetadata? metadata = await this.innerClient.GetInstancesAsync( instanceId, getInputsAndOutputs); return new WorkflowState(metadata); diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index d14113eeb..6e384954f 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -53,7 +53,13 @@ static bool TryGetGrpcAddress(out string address) string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); if (int.TryParse(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"), out int daprGrpcPort)) { + // There is a bug in the Durable Task SDK that requires us to change the format of the address + // depending on the version of .NET that we're targeting. For now, we work around this manually. +#if NET6_0_OR_GREATER + address = $"http://localhost:{daprGrpcPort}"; +#else address = $"localhost:{daprGrpcPort}"; +#endif return true; } diff --git a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj index 3df269709..10ae69b38 100644 --- a/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj +++ b/test/Dapr.E2E.Test/Dapr.E2E.Test.csproj @@ -4,7 +4,7 @@ - + From 913bb7f24fd2faa1e0cc19fb200ca8bc2912f65d Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 3 Feb 2023 15:32:51 -0800 Subject: [PATCH 07/17] Remove Web APIs and web dependencies Signed-off-by: Ryan Lettieri --- .../Activities/NotifyActivity.cs | 1 + .../Activities/ProcessPaymentActivity.cs | 1 + .../Activities/ReserveInventoryActivity.cs | 1 + .../Activities/UpdateInventoryActivity.cs | 4 +- examples/Workflow/WorkflowWebApp/Program.cs | 95 ++++--------------- .../WorkflowWebApp/WorkflowWebApp.csproj | 2 +- 6 files changed, 23 insertions(+), 81 deletions(-) diff --git a/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs index 44846c20e..3fe786458 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs @@ -2,6 +2,7 @@ { using System.Threading.Tasks; using Dapr.Workflow; + using Microsoft.Extensions.Logging; record Notification(string Message); diff --git a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs index 47d1421c9..af2e8c4a9 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; + using Microsoft.Extensions.Logging; using WorkflowWebApp.Models; class ProcessPaymentActivity : WorkflowActivity diff --git a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs index 1d523d688..040db81da 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; + using Microsoft.Extensions.Logging; using WorkflowWebApp.Models; class ReserveInventoryActivity : WorkflowActivity diff --git a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs index 6766c3392..6e65060dd 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs @@ -1,10 +1,10 @@ -namespace WorkflowWebApp.Activities +namespace WorkflowWebApp.Activities { using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; using WorkflowWebApp.Models; - using DurableTask.Core.Exceptions; + using Microsoft.Extensions.Logging; class UpdateInventoryActivity : WorkflowActivity { diff --git a/examples/Workflow/WorkflowWebApp/Program.cs b/examples/Workflow/WorkflowWebApp/Program.cs index 1c65aa298..8a7089a4e 100644 --- a/examples/Workflow/WorkflowWebApp/Program.cs +++ b/examples/Workflow/WorkflowWebApp/Program.cs @@ -1,96 +1,35 @@ using Dapr.Client; using Dapr.Workflow; -using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; -using Microsoft.AspNetCore.Mvc; -using System.Text.Json.Serialization; using WorkflowWebApp.Activities; using WorkflowWebApp.Models; using WorkflowWebApp.Workflows; using System.Text.Json; +using Microsoft.Extensions.Hosting; const string workflowComponent = "dapr"; const string storeName = "statestore"; const string workflowName = nameof(OrderProcessingWorkflow); // The workflow host is a background service that connects to the sidecar over gRPC -var builder = WebApplication.CreateBuilder(args); - -// Dapr workflows are registered as part of the service configuration -builder.Services.AddDaprWorkflow(options => -{ - // Note that it's also possible to register a lambda function as the workflow - // or activity implementation instead of a class. - options.RegisterWorkflow(); - - // These are the activities that get invoked by the workflow(s). - options.RegisterActivity(); - options.RegisterActivity(); - options.RegisterActivity(); - options.RegisterActivity(); -}); - -WebApplication app = builder.Build(); - - -// POST starts new order workflow instance -app.MapPost("/orders", async (DaprClient client, [FromBody] OrderPayload orderInfo) => -{ - // Generate a unique ID for the workflow - string orderId = Guid.NewGuid().ToString()[..8]; - - if (orderInfo?.Name == null) - { - return Results.BadRequest(new - { - message = "Order data was missing from the request", - example = new OrderPayload("Paperclips", 99.95), - }); - } - - // Start the workflow - var response = await client.StartWorkflowAsync(orderId, workflowComponent, workflowName, orderInfo, null, CancellationToken.None); - - // return an HTTP 202 and a Location header to be used for status query - return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }); -}); - -// GET fetches state for order workflow to report status -app.MapGet("/orders/{orderId}", async (string orderId, DaprClient client) => +var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => { - var state = await client.GetWorkflowAsync(orderId, workflowComponent, workflowName); - - if (string.IsNullOrEmpty(state.instanceId)) - { - return Results.NotFound($"No order with ID = '{orderId}' was found."); - } - - var httpResponsePayload = new - { - status = state.metadata["dapr.workflow.runtime_status"].ToString(), - }; - - if (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING" || state.metadata["dapr.workflow.runtime_status"].ToString() == "PENDING") + services.AddDaprWorkflow(options => { - // HTTP 202 Accepted - async polling clients should keep polling for status - return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }, httpResponsePayload); - } - else - { - // HTTP 200 OK - return Results.Ok(httpResponsePayload); - } -}).WithName("GetOrderInfoEndpoint"); - - -app.MapPost("/reset", [Obsolete] async (DaprClient client) => -{ - // Save a bunch of items in the state store - await client.SaveStateAsync("statestore", "Paperclips", new InventoryItem(Name: "Paperclips", TotalCost: 99.95, Quantity: 100)); - - return Results.Ok(); + // Note that it's also possible to register a lambda function as the workflow + // or activity implementation instead of a class. + options.RegisterWorkflow(); + + // These are the activities that get invoked by the workflow(s). + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + options.RegisterActivity(); + }); }); -app.Start(); +// Start the app - this is the point where we connect to the Dapr sidecar +using var host = builder.Build(); +host.Start(); // Start the client string daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); @@ -170,4 +109,4 @@ void RestockInventory() daprClient.SaveStateAsync(storeName, "Cars", new OrderPayload(Name: "Cars", TotalCost: 15000, Quantity: 100)); daprClient.SaveStateAsync(storeName, "Computers", new OrderPayload(Name: "Computers", TotalCost: 500, Quantity: 100)); return; -} \ No newline at end of file +} diff --git a/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj b/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj index 23f4f1f7d..0c40eea0c 100644 --- a/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj +++ b/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj @@ -1,4 +1,4 @@ - + From 1e5c9c76b428d9d6562c711cd42786a5dbcbbf31 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Thu, 2 Feb 2023 15:55:17 -0700 Subject: [PATCH 08/17] Renaming WorkflowWebApp to WorkflowConsoleApp Signed-off-by: Ryan Lettieri --- all.sln | 2 +- examples/Workflow/README.md | 12 ++++++------ .../Activities/NotifyActivity.cs | 2 +- .../Activities/ProcessPaymentActivity.cs | 4 ++-- .../Activities/ReserveInventoryActivity.cs | 4 ++-- .../Activities/UpdateInventoryActivity.cs | 4 ++-- .../{WorkflowWebApp => WorkflowConsoleApp}/Models.cs | 2 +- .../Program.cs | 7 +++---- .../Properties/launchSettings.json | 0 .../WorkflowConsoleApp.csproj} | 0 .../Workflows/OrderProcessingWorkflow.cs | 6 +++--- .../{WorkflowWebApp => WorkflowConsoleApp}/demo.http | 0 12 files changed, 21 insertions(+), 22 deletions(-) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Activities/NotifyActivity.cs (93%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Activities/ProcessPaymentActivity.cs (93%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Activities/ReserveInventoryActivity.cs (96%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Activities/UpdateInventoryActivity.cs (96%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Models.cs (92%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Program.cs (95%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Properties/launchSettings.json (100%) rename examples/Workflow/{WorkflowWebApp/WorkflowWebApp.csproj => WorkflowConsoleApp/WorkflowConsoleApp.csproj} (100%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/Workflows/OrderProcessingWorkflow.cs (95%) rename examples/Workflow/{WorkflowWebApp => WorkflowConsoleApp}/demo.http (100%) diff --git a/all.sln b/all.sln index b252ced32..b57f28024 100644 --- a/all.sln +++ b/all.sln @@ -91,7 +91,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow", "src\Dapr.W EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflow", "Workflow", "{BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowWebApp", "examples\Workflow\WorkflowWebApp\WorkflowWebApp.csproj", "{5C61ABED-7623-4C28-A5C9-C5972A0F669C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowConsoleApp", "examples\Workflow\WorkflowConsoleApp\WorkflowConsoleApp.csproj", "{5C61ABED-7623-4C28-A5C9-C5972A0F669C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishSubscribe", "PublishSubscribe", "{0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}" EndProject diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index c04663ce9..42ba37622 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -11,13 +11,13 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and ## Projects in sample -This sample contains a single [WorkflowWebApp](./WorkflowWebApp) ASP.NET Core project. It combines both the workflow implementations and the web APIs for starting and querying workflows instances. +This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) ASP.NET Core project. It combines both the workflow implementations and the web APIs for starting and querying workflows instances. The main `Program.cs` file contains the main setup of the app, including the registration of the web APIs and the registration of the workflow and workflow activities. The workflow definition is found in `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. ## Running the example -To run the workflow web app locally, run this command in the `WorkflowWebApp` directory: +To run the workflow web app locally, run this command in the `WorkflowConsoleApp` directory: ```sh dapr run --app-id wfwebapp --dapr-http-port 3500 -- dotnet run @@ -118,12 +118,12 @@ Transfer-Encoding: chunked When the workflow has completed, the stdout of the web app should look like the following: ```log -info: WorkflowWebApp.Activities.NotifyActivity[0] +info: WorkflowConsoleApp.Activities.NotifyActivity[0] Received order cdcce425 for Paperclips at $99.95 -info: WorkflowWebApp.Activities.ReserveInventoryActivity[0] +info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0] Reserving inventory: cdcce425, Paperclips, 1 -info: WorkflowWebApp.Activities.ProcessPaymentActivity[0] +info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0] Processing payment: cdcce425, 99.95, USD -info: WorkflowWebApp.Activities.NotifyActivity[0] +info: WorkflowConsoleApp.Activities.NotifyActivity[0] Order cdcce425 processed successfully! ``` diff --git a/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs similarity index 93% rename from examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs rename to examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs index 3fe786458..947450325 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs @@ -1,4 +1,4 @@ -namespace WorkflowWebApp.Activities +namespace WorkflowConsoleApp.Activities { using System.Threading.Tasks; using Dapr.Workflow; diff --git a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs similarity index 93% rename from examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs rename to examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index af2e8c4a9..e0eebcf70 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,10 +1,10 @@ -namespace WorkflowWebApp.Activities +namespace WorkflowConsoleApp.Activities { using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; - using WorkflowWebApp.Models; + using WorkflowConsoleApp.Models; class ProcessPaymentActivity : WorkflowActivity { diff --git a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs similarity index 96% rename from examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs rename to examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 040db81da..83fb2dbcd 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -1,10 +1,10 @@ -namespace WorkflowWebApp.Activities +namespace WorkflowConsoleApp.Activities { using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; using Microsoft.Extensions.Logging; - using WorkflowWebApp.Models; + using WorkflowConsoleApp.Models; class ReserveInventoryActivity : WorkflowActivity { diff --git a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs similarity index 96% rename from examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs rename to examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 6e65060dd..20fdf6e6d 100644 --- a/examples/Workflow/WorkflowWebApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,9 +1,9 @@ -namespace WorkflowWebApp.Activities +namespace WorkflowConsoleApp.Activities { using System.Threading.Tasks; using Dapr.Client; using Dapr.Workflow; - using WorkflowWebApp.Models; + using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; class UpdateInventoryActivity : WorkflowActivity diff --git a/examples/Workflow/WorkflowWebApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs similarity index 92% rename from examples/Workflow/WorkflowWebApp/Models.cs rename to examples/Workflow/WorkflowConsoleApp/Models.cs index c36147a79..82bf71931 100644 --- a/examples/Workflow/WorkflowWebApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -1,4 +1,4 @@ -namespace WorkflowWebApp.Models +namespace WorkflowConsoleApp.Models { record OrderPayload(string Name, double TotalCost, int Quantity = 1); record InventoryRequest(string RequestId, string ItemName, int Quantity); diff --git a/examples/Workflow/WorkflowWebApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs similarity index 95% rename from examples/Workflow/WorkflowWebApp/Program.cs rename to examples/Workflow/WorkflowConsoleApp/Program.cs index 8a7089a4e..51a453721 100644 --- a/examples/Workflow/WorkflowWebApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -1,8 +1,8 @@ using Dapr.Client; using Dapr.Workflow; -using WorkflowWebApp.Activities; -using WorkflowWebApp.Models; -using WorkflowWebApp.Workflows; +using WorkflowConsoleApp.Activities; +using WorkflowConsoleApp.Models; +using WorkflowConsoleApp.Workflows; using System.Text.Json; using Microsoft.Extensions.Hosting; @@ -86,7 +86,6 @@ while (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING") { await Task.Delay(TimeSpan.FromSeconds(5)); - Console.WriteLine("Your workflow is in progress. Here is the status of the workflow: {0}", state); state = await daprClient.GetWorkflowAsync(orderId, workflowComponent, workflowName); } Console.WriteLine("Your workflow has completed: {0}", JsonSerializer.Serialize(state)); diff --git a/examples/Workflow/WorkflowWebApp/Properties/launchSettings.json b/examples/Workflow/WorkflowConsoleApp/Properties/launchSettings.json similarity index 100% rename from examples/Workflow/WorkflowWebApp/Properties/launchSettings.json rename to examples/Workflow/WorkflowConsoleApp/Properties/launchSettings.json diff --git a/examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj b/examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj similarity index 100% rename from examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj rename to examples/Workflow/WorkflowConsoleApp/WorkflowConsoleApp.csproj diff --git a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs similarity index 95% rename from examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs rename to examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 18b2b2952..68de9b4fd 100644 --- a/examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,10 +1,10 @@ -namespace WorkflowWebApp.Workflows +namespace WorkflowConsoleApp.Workflows { using System.Threading.Tasks; using Dapr.Workflow; using DurableTask.Core.Exceptions; - using WorkflowWebApp.Activities; - using WorkflowWebApp.Models; + using WorkflowConsoleApp.Activities; + using WorkflowConsoleApp.Models; class OrderProcessingWorkflow : Workflow { diff --git a/examples/Workflow/WorkflowWebApp/demo.http b/examples/Workflow/WorkflowConsoleApp/demo.http similarity index 100% rename from examples/Workflow/WorkflowWebApp/demo.http rename to examples/Workflow/WorkflowConsoleApp/demo.http From 46bab78466ba0708756340d6b9ad2b4b12d19add Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Fri, 3 Feb 2023 18:41:23 -0800 Subject: [PATCH 09/17] Various updates to the sample app - Replaced DaprClient with WorkflowEngineClient - Removed unused etag logic - Fixed incorrect usage of certain model types - Cleaned up logs and console output - Simplified program loop - Cleaned up console output and added some coloring - Added error handling in the console interactions - Various other tweaks/simplifications/enhancements Signed-off-by: Chris Gillum Signed-off-by: Ryan Lettieri --- .../Activities/ProcessPaymentActivity.cs | 2 +- .../Activities/ReserveInventoryActivity.cs | 25 +-- .../Activities/UpdateInventoryActivity.cs | 28 ++- .../Workflow/WorkflowConsoleApp/Models.cs | 10 +- .../Workflow/WorkflowConsoleApp/Program.cs | 209 +++++++++++++----- .../Workflows/OrderProcessingWorkflow.cs | 2 - 6 files changed, 184 insertions(+), 92 deletions(-) diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index e0eebcf70..d540c741d 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -23,7 +23,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay "Processing payment: {requestId} for {amount} {item} at ${currency}", req.RequestId, req.Amount, - req.ItemBeingPruchased, + req.ItemName, req.Currency); // Simulate slow processing diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 83fb2dbcd..99bc288f5 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -21,40 +21,39 @@ public ReserveInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) public override async Task RunAsync(WorkflowActivityContext context, InventoryRequest req) { this.logger.LogInformation( - "Reserving inventory for order {requestId} of {quantity} {name}", + "Reserving inventory for order '{requestId}' of {quantity} {name}", req.RequestId, req.Quantity, req.ItemName); - OrderPayload orderResponse; - string key; - // Ensure that the store has items - (orderResponse, key) = await client.GetStateAndETagAsync(storeName, req.ItemName); + InventoryItem item = await client.GetStateAsync( + storeName, + req.ItemName); // Catch for the case where the statestore isn't setup - if (orderResponse == null) + if (item == null) { // Not enough items. - return new InventoryResult(false, orderResponse); + return new InventoryResult(false, item); } this.logger.LogInformation( - "There are: {requestId}, {name} available for purchase", - orderResponse.Quantity, - orderResponse.Name); + "There are {quantity} {name} available for purchase", + item.Quantity, + item.Name); // See if there're enough items to purchase - if (orderResponse.Quantity >= req.Quantity) + if (item.Quantity >= req.Quantity) { // Simulate slow processing await Task.Delay(TimeSpan.FromSeconds(2)); - return new InventoryResult(true, orderResponse); + return new InventoryResult(true, item); } // Not enough items. - return new InventoryResult(false, orderResponse); + return new InventoryResult(false, item); } } diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 20fdf6e6d..3f9a55c85 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -6,7 +6,7 @@ using WorkflowConsoleApp.Models; using Microsoft.Extensions.Logging; - class UpdateInventoryActivity : WorkflowActivity + class UpdateInventoryActivity : WorkflowActivity { static readonly string storeName = "statestore"; readonly ILogger logger; @@ -18,21 +18,22 @@ public UpdateInventoryActivity(ILoggerFactory loggerFactory, DaprClient client) this.client = client; } - public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) + public override async Task RunAsync(WorkflowActivityContext context, PaymentRequest req) { this.logger.LogInformation( - "Checking Inventory for: Order# {requestId} for {amount} {item}", + "Checking inventory for order '{requestId}' for {amount} {name}", req.RequestId, req.Amount, - req.ItemBeingPruchased); + req.ItemName); // Simulate slow processing await Task.Delay(TimeSpan.FromSeconds(5)); // Determine if there are enough Items for purchase - var (original, originalETag) = await client.GetStateAndETagAsync(storeName, req.ItemBeingPruchased); - int newQuantity = original.Quantity - req.Amount; - + InventoryItem item = await client.GetStateAsync( + storeName, + req.ItemName); + int newQuantity = item.Quantity - req.Amount; if (newQuantity < 0) { this.logger.LogInformation( @@ -41,9 +42,16 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay throw new InvalidOperationException(); } - // Update the statestore with the new amount of paper clips - await client.SaveStateAsync(storeName, req.ItemBeingPruchased, new OrderPayload(Name: req.ItemBeingPruchased, TotalCost: req.Currency, Quantity: newQuantity)); - this.logger.LogInformation($"There are now: {newQuantity} {original.Name} left in stock"); + // Update the statestore with the new amount of the item + await client.SaveStateAsync( + storeName, + req.ItemName, + new InventoryItem(Name: req.ItemName, PerItemCost: item.PerItemCost, Quantity: newQuantity)); + + this.logger.LogInformation( + "There are now {quantity} {name} left in stock", + newQuantity, + item.Name); return null; } diff --git a/examples/Workflow/WorkflowConsoleApp/Models.cs b/examples/Workflow/WorkflowConsoleApp/Models.cs index 82bf71931..9f3720cb0 100644 --- a/examples/Workflow/WorkflowConsoleApp/Models.cs +++ b/examples/Workflow/WorkflowConsoleApp/Models.cs @@ -1,9 +1,9 @@ -namespace WorkflowConsoleApp.Models +namespace WorkflowConsoleApp.Models { record OrderPayload(string Name, double TotalCost, int Quantity = 1); record InventoryRequest(string RequestId, string ItemName, int Quantity); - record InventoryResult(bool Success, OrderPayload orderPayload); - record PaymentRequest(string RequestId, string ItemBeingPruchased, int Amount, double Currency); + record InventoryResult(bool Success, InventoryItem orderPayload); + record PaymentRequest(string RequestId, string ItemName, int Amount, double Currency); record OrderResult(bool Processed); - record InventoryItem(string Name, double TotalCost, int Quantity); -} \ No newline at end of file + record InventoryItem(string Name, double PerItemCost, int Quantity); +} diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index 51a453721..b1235c37c 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -3,12 +3,10 @@ using WorkflowConsoleApp.Activities; using WorkflowConsoleApp.Models; using WorkflowConsoleApp.Workflows; -using System.Text.Json; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; -const string workflowComponent = "dapr"; const string storeName = "statestore"; -const string workflowName = nameof(OrderProcessingWorkflow); // The workflow host is a background service that connects to the sidecar over gRPC var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services => @@ -27,85 +25,174 @@ }); }); -// Start the app - this is the point where we connect to the Dapr sidecar +// Dapr uses a random port for gRPC by default. If we don't know what that port +// is (because this app was started separate from dapr), then assume 4001. +if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"))) +{ + Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "4001"); +} + +Console.ForegroundColor = ConsoleColor.White; +Console.WriteLine("*** Welcome to the Dapr Workflow Console App sample!"); +Console.WriteLine("*** Using this app, you will place orders that start workflows."); +Console.WriteLine("*** Ensure that dapr is running in a separate teminal window using the following command:"); +Console.ForegroundColor = ConsoleColor.Green; +Console.WriteLine(" dapr run --dapr-grpc-port 4001 --app-id wfapp"); +Console.WriteLine(); +Console.ResetColor(); + +// Start the app - this is the point where we connect to the Dapr sidecar to +// listen for workflow work-items to execute. using var host = builder.Build(); host.Start(); -// Start the client -string daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); -if (string.IsNullOrEmpty(daprPortStr)) +using var daprClient = new DaprClientBuilder().Build(); + +// Wait for the sidecar to become available +while (!await daprClient.CheckHealthAsync()) { - Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "4001"); + Thread.Sleep(TimeSpan.FromSeconds(5)); } -using var daprClient = new DaprClientBuilder().Build(); -// Start the console app +// Wait one more second for the workflow engine to finish initializing. +// This is just to make the log output look a little nicer. +Thread.Sleep(TimeSpan.FromSeconds(1)); + +// NOTE: WorkflowEngineClient will be replaced with a richer version of DaprClient +// in a subsequent SDK release. This is a temporary workaround. +WorkflowEngineClient workflowClient = host.Services.GetRequiredService(); + +var baseInventory = new List +{ + new InventoryItem(Name: "Paperclips", PerItemCost: 5, Quantity: 100), + new InventoryItem(Name: "Cars", PerItemCost: 15000, Quantity: 100), + new InventoryItem(Name: "Computers", PerItemCost: 500, Quantity: 100), +}; + +// Populate the store with items +await RestockInventory(daprClient, baseInventory); + +// Start the input loop while (true) { - var health = await daprClient.CheckHealthAsync(); - Console.WriteLine("Welcome to the workflows example!"); - Console.WriteLine("In this example, you will be starting a workflow and obtaining the status."); - Console.WriteLine("First, ensure that dapr is running and connected to the correct port"); - Console.WriteLine("To do this, run the following command in a separate teminal window: dapr run --dapr-grpc-port 4001 --app-id wfwebapp"); - - // tell the user whats already in store - Console.WriteLine("For starters, the store has been stocked with 100 'Cars', 100 'Paperclips' and 100 'Computers':"); - // Populate the store with items - RestockInventory(); - - // Main Loop - string continueChar = "1"; - while (continueChar == "1") + // 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($"Please 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)) { - // Generate a unique ID for the workflow - string orderId = Guid.NewGuid().ToString()[..8]; - Console.WriteLine("To get started, enter an item you would like to purchase:"); - var itemToPurchase = Console.ReadLine(); - Console.WriteLine("You are going to purchase: {0}", itemToPurchase); - Console.WriteLine("Next, enter the quantity of {0} that you want to purchase: ", itemToPurchase); - var ammountToPurchase = Convert.ToInt32(Console.ReadLine()); + continue; + } + else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase)) + { + await RestockInventory(daprClient, baseInventory); + continue; + } - // Construct the order - OrderPayload orderInfo = new OrderPayload(itemToPurchase, 99.95, ammountToPurchase); + 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("Before check inventory!"); + 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; + } - OrderPayload orderResponse; - string key; - // Ensure that the store has items - (orderResponse, key) = await daprClient.GetStateAndETagAsync(storeName, itemToPurchase); + // Construct the order with a unique order ID + string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}"; + double totalCost = amount * item.PerItemCost; + var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount); - Console.WriteLine("After check inventory: {0}", orderResponse); + // Start the workflow using the order ID as the workflow ID + Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); + await workflowClient.ScheduleNewWorkflowAsync( + name: nameof(OrderProcessingWorkflow), + instanceId: orderId, + input: orderInfo); - // Start the workflow - Console.WriteLine("Starting workflow {0} purchasing {1} {2}", orderId, ammountToPurchase, itemToPurchase); - var response = await daprClient.StartWorkflowAsync(orderId, workflowComponent, workflowName, orderInfo, null, CancellationToken.None); + // Wait for the workflow to complete + WorkflowState state = await workflowClient.GetWorkflowStateAsync( + instanceId: orderId, + getInputsAndOutputs: true); + while (!state.IsWorkflowCompleted) + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + state = await workflowClient.GetWorkflowStateAsync( + instanceId: orderId, + getInputsAndOutputs: true); + } - var state = await daprClient.GetWorkflowAsync(orderId, workflowComponent, workflowName); - Console.WriteLine("Your workflow has started. Here is the status of the workflow: {0}", state); - while (state.metadata["dapr.workflow.runtime_status"].ToString() == "RUNNING") + if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) + { + OrderResult result = state.ReadOutputAs(); + if (result.Processed) { - await Task.Delay(TimeSpan.FromSeconds(5)); - state = await daprClient.GetWorkflowAsync(orderId, workflowComponent, workflowName); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed succesfully."); + Console.ResetColor(); } - Console.WriteLine("Your workflow has completed: {0}", JsonSerializer.Serialize(state)); - Console.WriteLine("To start another workflow, press 1. If you'd like to restock the store, press 2. Otherwise, you can exit the demo with any other character"); - continueChar = Console.ReadLine(); - - if (continueChar == "2") + else { - RestockInventory(); - continueChar = "1"; + Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); } } - - break; + else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) + { + // WorkflowEngineClient doesn't expose a way to get error information. + // For that, we resort to DaprClient. The experience will be improved in the next release. + GetWorkflowResponse response = await daprClient.GetWorkflowAsync( + instanceId: orderId, + workflowComponent: "dapr", + workflowName: nameof(OrderProcessingWorkflow), + CancellationToken.None); + + string failureDetails = await GetWorkflowFailureDetails(daprClient, orderId); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"The workflow failed - {failureDetails}"); + Console.ResetColor(); + } + + Console.WriteLine(); } -void RestockInventory() +static async Task RestockInventory(DaprClient daprClient, List inventory) { - daprClient.SaveStateAsync(storeName, "Paperclips", new OrderPayload(Name: "PaperClips", TotalCost: 5, Quantity: 100)); - daprClient.SaveStateAsync(storeName, "Cars", new OrderPayload(Name: "Cars", TotalCost: 15000, Quantity: 100)); - daprClient.SaveStateAsync(storeName, "Computers", new OrderPayload(Name: "Computers", TotalCost: 500, Quantity: 100)); - return; + Console.WriteLine("*** Restocking inventory..."); + foreach (var item in inventory) + { + Console.WriteLine($"*** \t{item.Name}: {item.Quantity}"); + await daprClient.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item); + } +} + +static async Task GetWorkflowFailureDetails(DaprClient daprClient, string orderId) +{ + // Use DaprClient to get the error details + GetWorkflowResponse response = await daprClient.GetWorkflowAsync( + instanceId: orderId, + workflowComponent: "dapr", + workflowName: nameof(OrderProcessingWorkflow), + CancellationToken.None); + + // Available metadata fields: https://github.com/dapr/dapr/blob/ad4c38756fa08dda5def8f0a7a6d672feb37be91/pkg/runtime/wfengine/component.go#L146-L161 + if (response.metadata.TryGetValue("dapr.workflow.failure.error_type", out string errorType) && + response.metadata.TryGetValue("dapr.workflow.failure.error_message", out string errorMessage)) + { + return $"{errorType}: {errorMessage}"; + } + else + { + return ":("; + } } diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 68de9b4fd..53453d6b2 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -17,8 +17,6 @@ await context.CallActivityAsync( nameof(NotifyActivity), new Notification($"Received order {orderId} for {order.Quantity} {order.Name} at ${order.TotalCost}")); - string requestId = context.InstanceId; - // Determine if there is enough of the item available for purchase by checking the inventory InventoryResult result = await context.CallActivityAsync( nameof(ReserveInventoryActivity), From 2dbf179a2167ed52098e9747f5dd69136a680b97 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Thu, 2 Feb 2023 16:46:55 -0700 Subject: [PATCH 10/17] Updates to README and demo http commands Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 114 ++++++++++-------- .../Workflow/WorkflowConsoleApp/demo.http | 19 +-- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 42ba37622..72e98ca3e 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -1,6 +1,6 @@ # Dapr Workflow with ASP.NET Core sample -This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and invoke it using ASP.NET Core web APIs. +This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and invoke it using the console. ## Prerequisites @@ -11,87 +11,91 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and ## Projects in sample -This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) ASP.NET Core project. It combines both the workflow implementations and the web APIs for starting and querying workflows instances. +This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) ASP.NET Core project. It utilizes the workflow implementations starting and querying workflows instances. -The main `Program.cs` file contains the main setup of the app, including the registration of the web APIs and the registration of the workflow and workflow activities. The workflow definition is found in `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. +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 `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. ## Running the example -To run the workflow web app locally, run this command in the `WorkflowConsoleApp` directory: +To run the workflow web app locally, two separate terminal windows are required. +In the first terminal window, down the `WorkflowConsoleApp` directory, run the following command to start the program itself: ```sh -dapr run --app-id wfwebapp --dapr-http-port 3500 -- dotnet run +dotnet run ``` -The application will listen for HTTP requests at `http://localhost:10080`. +Next, in a separate terminal window, start the dapr sidecar: -This workflow example utilizes a redis statestore. In order to retrieve items from this statestore, an HTTP command must first be sent down to restock the inventory: +```sh +dapr run --app-id wfwebapp --dapr-grpc-port 4001 --dapr-http-port 3500 +``` -On Linux/macOS (bash): +Dapr will listen for HTTP requests at `http://localhost:3500`. -```bash -curl -i -X POST http://localhost:10080/reset -``` +This workflow example utilizes a redis statestore to simulate the purchasing of items and restocking of inventory. The console prompts will provide directions on how to both purchase and restock items. -On Windows (PowerShell): +To start a workflow, you have two options: +Option A: Follow the directions from the console prompts. -```powershell -curl -i -X POST http://localhost:10080/reset ` -``` +Option B: Use the workflows API and send a request to Dapr directly. Examples are included below as well as in the "demo.http" file down the "WorkflowConsoleApp" directory. -To start a workflow, use the following command to send an HTTP POST request, which triggers an HTTP API that starts the workflow using the Dapr Workflow client. Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. +Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. -On Linux/macOS (bash): +Make note of the "xxx" 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:10080/orders \ +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start \ -H "Content-Type: application/json" \ - -d '{"name": "Paperclips", "totalCost": 99.95, "quantity": 1}' + -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' ``` On Windows (PowerShell): ```powershell -curl -i -X POST http://localhost:10080/orders ` +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start ` -H "Content-Type: application/json" ` - -d '{"name": "Paperclips", "totalCost": 99.95, "quantity": 1}' + -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' ``` -If successful, you should see a response like the following, which contains a `Location` header pointing to a status endpoint for the workflow that was created with a randomly generated 8-digit ID: +If successful, you should see a response like the following, which contains a `Location` header pointing to a status endpoint for the workflow that was created with the identifier that you provided. ```http HTTP/1.1 202 Accepted -Content-Length: 0 -Date: Tue, 24 Jan 2023 00:02:02 GMT -Server: Kestrel -Location: http://localhost:10080/orders/cdcce425 +Date: Thu, 02 Feb 2023 23:34:53 GMT +Content-Type: application/json +Content-Length: 21 +Traceparent: 00-37c66ddb4b7f7921b28820fa06489239-0046c6e7a3403139-01 ``` Next, send an HTTP request to the URL in the `Location` header in the previous HTTP response, like in the following example: ```bash -curl -i http://localhost:10080/orders/cdcce425 +curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx ``` If the workflow has completed running, you should see the following output (formatted for readability): ```http -HTTP/1.1 200 OK -Content-Type: application/json; charset=utf-8 -Date: Tue, 24 Jan 2023 00:10:53 GMT -Server: Kestrel -Transfer-Encoding: chunked +HTTP/1.1 202 Accepted +Date: Thu, 02 Feb 2023 23:43:27 GMT +Content-Type: application/json +Content-Length: 387 +Traceparent: 00-dee54b8d7cdd4232938a5c4d30504b77-02e409f4c9fd6a14-01 +Connection: close { - "details": { - "name": "Paperclips", - "quantity": 1, - "totalCost": 99.95 - }, - "result": { - "processed": true - }, - "status": "Completed" + "WFInfo": { + "instance_id": "xxx" + }, + "start_time": "2023-02-02T23:34:53Z", + "metadata": { + "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" + } } ``` @@ -99,19 +103,25 @@ If the workflow hasn't completed yet, you might instead see the following: ```http HTTP/1.1 202 Accepted -Content-Type: application/json; charset=utf-8 -Date: Tue, 24 Jan 2023 00:17:49 GMT -Location: http://localhost:10080/orders/cdcce425 -Server: Kestrel -Transfer-Encoding: chunked +Date: Thu, 02 Feb 2023 23:43:27 GMT +Content-Type: application/json +Content-Length: 387 +Traceparent: 00-dee54b8d7cdd4232938a5c4d30504b77-02e409f4c9fd6a14-01 +Connection: close { - "details": { - "name": "Paperclips", - "quantity": 1, - "totalCost": 99.95 - }, - "status": "Running" + "WFInfo": { + "instance_id": "xxx" + }, + "start_time": "2023-02-02T23:34:53Z", + "metadata": { + "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" + } } ``` diff --git a/examples/Workflow/WorkflowConsoleApp/demo.http b/examples/Workflow/WorkflowConsoleApp/demo.http index 8d9a31859..bb91e5045 100644 --- a/examples/Workflow/WorkflowConsoleApp/demo.http +++ b/examples/Workflow/WorkflowConsoleApp/demo.http @@ -1,16 +1,17 @@ -### Reset workflow -POST http://localhost:10080/reset +### Start order processing workflow - replace xxx with any id you like +POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start Content-Type: application/json +{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}} -### Create new order -POST http://localhost:10080/orders +### Start order processing workflow - replace xxx with any id you like +POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/xxx/start Content-Type: application/json -{"name": "Paperclips", "totalCost": 99.95, "quantity": 1} +{ "input" : {"Name": "Cars", "TotalCost": 10000, "Quantity": 30}} -### Query placeholder - replace xxx with id from location when order is creates -GET http://localhost:10080/orders/xxx +### 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 -### Query dapr sidecar directly -GET http://localhost:3500/v1.0-alpha1/workflows/dapr/wfwebapp/xxx \ No newline at end of file +### 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 0783ab58c8d55a21160a94feb987c15163f1a061 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 6 Feb 2023 10:41:23 -0800 Subject: [PATCH 11/17] Make README copy/paste-able and some other minor tweaks Signed-off-by: Chris Gillum Signed-off-by: Ryan Lettieri --- all.sln | 7 +++-- examples/Workflow/README.md | 30 +++++++------------ .../Activities/ReserveInventoryActivity.cs | 2 +- .../Activities/UpdateInventoryActivity.cs | 4 +-- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/all.sln b/all.sln index b57f28024..16df5b3d7 100644 --- a/all.sln +++ b/all.sln @@ -90,14 +90,17 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Workflow", "src\Dapr.Workflow\Dapr.Workflow.csproj", "{07578B6C-9B96-4B3D-BA2E-7800EFCA7F99}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflow", "Workflow", "{BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}" + ProjectSection(SolutionItems) = preProject + examples\Workflow\README.md = examples\Workflow\README.md + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowConsoleApp", "examples\Workflow\WorkflowConsoleApp\WorkflowConsoleApp.csproj", "{5C61ABED-7623-4C28-A5C9-C5972A0F669C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PublishSubscribe", "PublishSubscribe", "{0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PublishEventExample", "examples\Client\PublishSubscribe\PublishEventExample\PublishEventExample.csproj", "{4A175C27-EAFE-47E7-90F6-873B37863656}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PublishEventExample", "examples\Client\PublishSubscribe\PublishEventExample\PublishEventExample.csproj", "{4A175C27-EAFE-47E7-90F6-873B37863656}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BulkPublishEventExample", "examples\Client\PublishSubscribe\BulkPublishEventExample\BulkPublishEventExample.csproj", "{DDC41278-FB60-403A-B969-2AEBD7C2D83C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", "examples\Client\PublishSubscribe\BulkPublishEventExample\BulkPublishEventExample.csproj", "{DDC41278-FB60-403A-B969-2AEBD7C2D83C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 72e98ca3e..6fd52b9c2 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -41,10 +41,10 @@ Option B: Use the workflows API and send a request to Dapr directly. Examples ar Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. -Make note of the "xxx" in the commands below. This represents the unique identifier for the workflow run and can be replaced with any identifier of your choosing. +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/xxx/start \ +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start \ -H "Content-Type: application/json" \ -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' ``` @@ -52,7 +52,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/xxx/start ` +curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234/start ` -H "Content-Type: application/json" ` -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' ``` @@ -63,14 +63,12 @@ If successful, you should see a response like the following, which contains a `L HTTP/1.1 202 Accepted Date: Thu, 02 Feb 2023 23:34:53 GMT Content-Type: application/json -Content-Length: 21 -Traceparent: 00-37c66ddb4b7f7921b28820fa06489239-0046c6e7a3403139-01 ``` -Next, send an HTTP request to the URL in the `Location` header in the previous HTTP response, like in the following example: +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/xxx +curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 ``` If the workflow has completed running, you should see the following output (formatted for readability): @@ -79,13 +77,10 @@ If the workflow has completed running, you should see the following output (form HTTP/1.1 202 Accepted Date: Thu, 02 Feb 2023 23:43:27 GMT Content-Type: application/json -Content-Length: 387 -Traceparent: 00-dee54b8d7cdd4232938a5c4d30504b77-02e409f4c9fd6a14-01 -Connection: close { "WFInfo": { - "instance_id": "xxx" + "instance_id": "1234" }, "start_time": "2023-02-02T23:34:53Z", "metadata": { @@ -105,13 +100,10 @@ If the workflow hasn't completed yet, you might instead see the following: HTTP/1.1 202 Accepted Date: Thu, 02 Feb 2023 23:43:27 GMT Content-Type: application/json -Content-Length: 387 -Traceparent: 00-dee54b8d7cdd4232938a5c4d30504b77-02e409f4c9fd6a14-01 -Connection: close { "WFInfo": { - "instance_id": "xxx" + "instance_id": "1234" }, "start_time": "2023-02-02T23:34:53Z", "metadata": { @@ -129,11 +121,11 @@ When the workflow has completed, the stdout of the web app should look like the ```log info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Received order cdcce425 for Paperclips at $99.95 + Received order 1234 for Paperclips at $99.95 info: WorkflowConsoleApp.Activities.ReserveInventoryActivity[0] - Reserving inventory: cdcce425, Paperclips, 1 + Reserving inventory: 1234, Paperclips, 1 info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0] - Processing payment: cdcce425, 99.95, USD + Processing payment: 1234, 99.95, USD info: WorkflowConsoleApp.Activities.NotifyActivity[0] - Order cdcce425 processed successfully! + Order 1234 processed successfully! ``` diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 99bc288f5..92035dfd6 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -29,7 +29,7 @@ public override async Task RunAsync(WorkflowActivityContext con // Ensure that the store has items InventoryItem item = await client.GetStateAsync( storeName, - req.ItemName); + req.ItemName.ToLowerInvariant()); // Catch for the case where the statestore isn't setup if (item == null) diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 3f9a55c85..922ed4d33 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -32,7 +32,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay // Determine if there are enough Items for purchase InventoryItem item = await client.GetStateAsync( storeName, - req.ItemName); + req.ItemName.ToLowerInvariant()); int newQuantity = item.Quantity - req.Amount; if (newQuantity < 0) { @@ -45,7 +45,7 @@ public override async Task RunAsync(WorkflowActivityContext context, Pay // Update the statestore with the new amount of the item await client.SaveStateAsync( storeName, - req.ItemName, + req.ItemName.ToLowerInvariant(), new InventoryItem(Name: req.ItemName, PerItemCost: item.PerItemCost, Quantity: newQuantity)); this.logger.LogInformation( From b91b3d795cc9e27896e2cbb1f09ed9dc0b6f6499 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Sun, 5 Feb 2023 13:34:46 -0700 Subject: [PATCH 12/17] Adding in Paul's devcontainer work Signed-off-by: Ryan Lettieri --- .devcontainer/Dockerfile | 29 +++++++++++++++++++ .devcontainer/devcontainer.json | 51 +++++++++++++++++++++++++++++++++ .devcontainer/localinit.sh | 9 ++++++ 3 files changed, 89 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/localinit.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..f8d470625 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,29 @@ +# +# 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. +# + +ARG VARIANT=bullseye +FROM mcr.microsoft.com/vscode/devcontainers/dotnet:dev-7.0-bullseye + +# Install minikube +RUN MINIKUBE_URL="https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64" \ + && sudo curl -sSL -o /usr/local/bin/minikube "${MINIKUBE_URL}" \ + && sudo chmod 0755 /usr/local/bin/minikube \ + && MINIKUBE_SHA256=$(curl -sSL "${MINIKUBE_URL}.sha256") \ + && echo "${MINIKUBE_SHA256} */usr/local/bin/minikube" | sha256sum -c - + + +# Install Dapr CLI +RUN wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash + +# Install Azure Dev CLI +RUN curl -fsSL https://aka.ms/install-azd.sh | bash \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e179c66b2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,51 @@ +{ + "name": "Azure Developer CLI", + "build": { + "dockerfile": "Dockerfile", + "args": { + "VARIANT": "bullseye" + } + }, + "features": { + "ghcr.io/devcontainers/features/azure-cli:1": { + "version": "2.38" + }, + "ghcr.io/devcontainers/features/docker-from-docker:1": { + "version": "20.10" + }, + "ghcr.io/devcontainers/features/dotnet:1": { + "version": "6.0" + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "2" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "16", + "nodeGypDependencies": false + } + }, + "extensions": [ + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker", + "ms-vscode.vscode-node-azure-pack", + "ms-dotnettools.csharp", + "ms-dotnettools.vscode-dotnet-runtime", + "ms-azuretools.vscode-dapr", + "GitHub.copilot" + ], + "forwardPorts": [ + 3000, + 3100, + 3500, + 3501, + 5000, + 5007 + ], + "postCreateCommand": ".devcontainer/localinit.sh", + "remoteUser": "vscode", + "hostRequirements": { + "memory": "8gb" + } + } + \ No newline at end of file diff --git a/.devcontainer/localinit.sh b/.devcontainer/localinit.sh new file mode 100644 index 000000000..80b27e4f4 --- /dev/null +++ b/.devcontainer/localinit.sh @@ -0,0 +1,9 @@ +# install Azure CLI extension for Container Apps +az config set extension.use_dynamic_install=yes_without_prompt +az extension add --name containerapp --yes + +# install Node.js and NPM LTS +nvm install v18.12.1 + +# initialize Dapr +dapr init --runtime-version=1.10.0-rc.2 \ No newline at end of file From 5c772ca20c2b67edfbffc4a72d2d253341677b44 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Mon, 6 Feb 2023 14:06:48 -0800 Subject: [PATCH 13/17] More README touch-ups Signed-off-by: Chris Gillum Signed-off-by: Ryan Lettieri --- examples/Workflow/README.md | 48 ++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 6fd52b9c2..2dc0ecab1 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -11,14 +11,14 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and ## Projects in sample -This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) ASP.NET Core project. It utilizes the workflow implementations starting and querying workflows instances. +This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. It utilizes the workflow authroring 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 `Workflows` directory and the workflow activity definitions are found in the `Activities` directory. +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. ## Running the example To run the workflow web app locally, two separate terminal windows are required. -In the first terminal window, down the `WorkflowConsoleApp` directory, run the following command to start the program itself: +In the first terminal window, from the `WorkflowConsoleApp` directory, run the following command to start the program itself: ```sh dotnet run @@ -27,19 +27,19 @@ dotnet run Next, in a separate terminal window, start the dapr sidecar: ```sh -dapr run --app-id wfwebapp --dapr-grpc-port 4001 --dapr-http-port 3500 +dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500 ``` Dapr will listen for HTTP requests at `http://localhost:3500`. -This workflow example utilizes a redis statestore to simulate the purchasing of items and restocking of inventory. The console prompts will provide directions on how to both purchase and restock items. +This example illustrates a purchase order processing workflow. The console prompts will provide directions on how to both purchase and restock items. To start a workflow, you have two options: -Option A: Follow the directions from the console prompts. -Option B: Use the workflows API and send a request to Dapr directly. Examples are included below as well as in the "demo.http" file down the "WorkflowConsoleApp" directory. +1. Follow the directions from the console prompts. +2. Use the workflows API and send a request to Dapr directly. Examples are included below as well as in the "demo.http" file down the "WorkflowConsoleApp" directory. -Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow. +For the workflow API option, two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is the purchase order information used as the input of the workflow. 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. @@ -57,12 +57,10 @@ curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessing -d '{ "input" : {"Name": "Paperclips", "TotalCost": 99.95, "Quantity": 1}}' ``` -If successful, you should see a response like the following, which contains a `Location` header pointing to a status endpoint for the workflow that was created with the identifier that you provided. +If successful, you should see a response like the following: -```http -HTTP/1.1 202 Accepted -Date: Thu, 02 Feb 2023 23:34:53 GMT -Content-Type: application/json +```json +{"instance_id":"1234"} ``` Next, send an HTTP request to get the status of the workflow that was started: @@ -71,13 +69,9 @@ Next, send an HTTP request to get the status of the workflow that was started: curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/OrderProcessingWorkflow/1234 ``` -If the workflow has completed running, you should see the following output (formatted for readability): - -```http -HTTP/1.1 202 Accepted -Date: Thu, 02 Feb 2023 23:43:27 GMT -Content-Type: application/json +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" @@ -89,18 +83,14 @@ Content-Type: application/json "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.runtime_status": "RUNNING" } } ``` -If the workflow hasn't completed yet, you might instead see the following: - -```http -HTTP/1.1 202 Accepted -Date: Thu, 02 Feb 2023 23:43:27 GMT -Content-Type: application/json +Once the workflow has completed running, you should see the following output, indicating that it has reached the "COMPLETED" status: +```json { "WFInfo": { "instance_id": "1234" @@ -112,12 +102,12 @@ Content-Type: application/json "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.runtime_status": "COMPLETED" } } ``` -When the workflow has completed, the stdout of the web app should look like the following: +Also, when the workflow has completed, the stdout of the workflow app should look like the following: ```log info: WorkflowConsoleApp.Activities.NotifyActivity[0] @@ -129,3 +119,5 @@ info: WorkflowConsoleApp.Activities.ProcessPaymentActivity[0] info: WorkflowConsoleApp.Activities.NotifyActivity[0] Order 1234 processed successfully! ``` + +If you have Zipkin configured for Dapr locally on your machine, then you should also be able to view the workflow trace spans in the Zipkin web UI (typically at http://localhost:9411/zipkin/). From c0abd08b3e170e43cef4e885fafe87518a7d0f7d Mon Sep 17 00:00:00 2001 From: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> Date: Wed, 8 Feb 2023 15:38:45 -0600 Subject: [PATCH 14/17] [docs] Add workflows to .NET client doc (#1019) * add workflows to client page Signed-off-by: Hannah Hunter Signed-off-by: Ryan Lettieri --- .../dotnet-sdk-docs/dotnet-client/_index.md | 21 +++++++++++++++++++ 1 file changed, 21 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 64bf1c787..0404857e8 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-client/_index.md @@ -199,12 +199,33 @@ await foreach (var items in subscribeConfigurationResponse.Source.WithCancellati } ``` +### Manage workflow instances (Alpha) + +```csharp +var daprClient = new DaprClientBuilder().Build(); + +string instanceId = "MyWorkflowInstance1"; +string workflowComponentName = "dapr"; // alternatively, this could be the name of a workflow component defined in yaml +string workflowName = "MyWorkflowDefinition"; +var input = new { name = "Billy", age = 30 }; // Any JSON-serializable value is OK + +// Start workflow +var startResponse = await daprClient.StartWorkflowAsync(instanceId, workflowComponentName, workflowName, input); + +// Terminate workflow +await daprClient.TerminateWorkflowAsync(instanceId, workflowComponentName); + +// Get workflow metadata +var getResponse = await daprClient.GetWorkflowAsync(instanceId, workflowComponentName, workflowName); +``` + ## Sidecar APIs ### Sidecar Health The .NET SDK provides a way to poll for the sidecar health, as well as a convenience method to wait for the sidecar to be ready. #### Poll for health This health endpoint returns true when both the sidecar and your application are up (fully initialized). + ```csharp var client = new DaprClientBuilder().Build(); From 8fa9f2517f0eca8f5f4e648ddc0980c9f75ed432 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Tue, 7 Feb 2023 15:56:25 -0700 Subject: [PATCH 15/17] Updating workflows readme and example Signed-off-by: Ryan Lettieri --- README.md | 3 ++- examples/Workflow/README.md | 10 +++++----- examples/Workflow/WorkflowConsoleApp/Program.cs | 10 +++++----- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a8338dfb7..b801ed01e 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,11 @@ This repo builds the following packages: - Dapr.Actors - Dapr.Actors.AspNetCore - Dapr.Extensions.Configuration +- Dapr.Workflow ### Prerequisites -Each project is a normal C# project. At minimum, you need [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0) to build, test, and generate NuGet packages. +Each project is a normal C# project. At minimum, you need [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0) and [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) to build, test, and generate NuGet packages. Also make sure to reference the [.NET SDK contribution guide](https://docs.dapr.io/contributing/sdk-contrib/dotnet-contributing/) diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index 2dc0ecab1..b1e7a1f93 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 ## Projects in sample -This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. It utilizes the workflow authroring SDK as well as the workflow management API for starting and querying workflows instances. +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. @@ -30,9 +30,9 @@ Next, in a separate terminal window, start the dapr sidecar: dapr run --app-id wfapp --dapr-grpc-port 4001 --dapr-http-port 3500 ``` -Dapr will listen for HTTP requests at `http://localhost:3500`. +Dapr listens for HTTP requests at `http://localhost:3500`. -This example illustrates a purchase order processing workflow. The console prompts will provide directions on how to both purchase and restock items. +This example illustrates a purchase order processing workflow. The console prompts provide directions on how to both purchase and restock items. To start a workflow, you have two options: @@ -107,7 +107,7 @@ Once the workflow has completed running, you should see the following output, in } ``` -Also, when the workflow has completed, the stdout of the workflow app should look like the following: +When the workflow has completed, the stdout of the workflow app should look like the following: ```log info: WorkflowConsoleApp.Activities.NotifyActivity[0] @@ -120,4 +120,4 @@ info: WorkflowConsoleApp.Activities.NotifyActivity[0] Order 1234 processed successfully! ``` -If you have Zipkin configured for Dapr locally on your machine, then you should also be able to 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/). diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index b1235c37c..2069103c0 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -33,9 +33,9 @@ } Console.ForegroundColor = ConsoleColor.White; -Console.WriteLine("*** Welcome to the Dapr Workflow Console App sample!"); -Console.WriteLine("*** Using this app, you will place orders that start workflows."); -Console.WriteLine("*** Ensure that dapr is running in a separate teminal window using the following command:"); +Console.WriteLine("*** Welcome to the Dapr Workflow console app sample!"); +Console.WriteLine("*** Using this app, you can place orders that start workflows."); +Console.WriteLine("*** Ensure that Dapr is running in a separate terminal window using the following command:"); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(" dapr run --dapr-grpc-port 4001 --app-id wfapp"); Console.WriteLine(); @@ -77,7 +77,7 @@ { // 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($"Please enter the name of one of the following items to order [{items}]."); + 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)) @@ -139,7 +139,7 @@ await workflowClient.ScheduleNewWorkflowAsync( if (result.Processed) { Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed succesfully."); + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully."); Console.ResetColor(); } else From 6754ee807d6a45726d4b0354cf6bcd488aeebe7f Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Tue, 7 Feb 2023 15:59:43 -0700 Subject: [PATCH 16/17] Fixing README for letting users know which .NET is needed Signed-off-by: Ryan Lettieri --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b801ed01e..54bb606dc 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ This repo builds the following packages: ### Prerequisites -Each project is a normal C# project. At minimum, you need [.NET 5.0 SDK](https://dotnet.microsoft.com/download/dotnet/5.0) and [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) to build, test, and generate NuGet packages. +Each project is a normal C# project. At minimum, you need [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet/6.0) to build, test, and generate NuGet packages. Also make sure to reference the [.NET SDK contribution guide](https://docs.dapr.io/contributing/sdk-contrib/dotnet-contributing/) From 35c1f1d448239a9dd5da60b5df1b6bfebab9ec05 Mon Sep 17 00:00:00 2001 From: Ryan Lettieri Date: Thu, 9 Feb 2023 09:03:20 -0700 Subject: [PATCH 17/17] moving using statements above the namespace Signed-off-by: Ryan Lettieri --- .../Activities/NotifyActivity.cs | 10 +++++----- .../Activities/ProcessPaymentActivity.cs | 14 +++++++------- .../Activities/ReserveInventoryActivity.cs | 14 +++++++------- .../Activities/UpdateInventoryActivity.cs | 14 +++++++------- .../Workflows/OrderProcessingWorkflow.cs | 14 +++++++------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs index 947450325..a6324947c 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/NotifyActivity.cs @@ -1,9 +1,9 @@ -namespace WorkflowConsoleApp.Activities -{ - using System.Threading.Tasks; - using Dapr.Workflow; - using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +namespace WorkflowConsoleApp.Activities +{ record Notification(string Message); class NotifyActivity : WorkflowActivity diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs index d540c741d..8132e7bee 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ProcessPaymentActivity.cs @@ -1,11 +1,11 @@ -namespace WorkflowConsoleApp.Activities -{ - using System.Threading.Tasks; - using Dapr.Client; - using Dapr.Workflow; - using Microsoft.Extensions.Logging; - using WorkflowConsoleApp.Models; +using System.Threading.Tasks; +using Dapr.Client; +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +using WorkflowConsoleApp.Models; +namespace WorkflowConsoleApp.Activities +{ class ProcessPaymentActivity : WorkflowActivity { readonly ILogger logger; diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs index 92035dfd6..48abca09e 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/ReserveInventoryActivity.cs @@ -1,11 +1,11 @@ -namespace WorkflowConsoleApp.Activities -{ - using System.Threading.Tasks; - using Dapr.Client; - using Dapr.Workflow; - using Microsoft.Extensions.Logging; - using WorkflowConsoleApp.Models; +using System.Threading.Tasks; +using Dapr.Client; +using Dapr.Workflow; +using Microsoft.Extensions.Logging; +using WorkflowConsoleApp.Models; +namespace WorkflowConsoleApp.Activities +{ class ReserveInventoryActivity : WorkflowActivity { readonly ILogger logger; diff --git a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs index 922ed4d33..d136e74cc 100644 --- a/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs +++ b/examples/Workflow/WorkflowConsoleApp/Activities/UpdateInventoryActivity.cs @@ -1,11 +1,11 @@ -namespace WorkflowConsoleApp.Activities -{ - using System.Threading.Tasks; - using Dapr.Client; - using Dapr.Workflow; - using WorkflowConsoleApp.Models; - using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using Dapr.Client; +using Dapr.Workflow; +using WorkflowConsoleApp.Models; +using Microsoft.Extensions.Logging; +namespace WorkflowConsoleApp.Activities +{ class UpdateInventoryActivity : WorkflowActivity { static readonly string storeName = "statestore"; diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 53453d6b2..b9b199a91 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,11 +1,11 @@ -namespace WorkflowConsoleApp.Workflows -{ - using System.Threading.Tasks; - using Dapr.Workflow; - using DurableTask.Core.Exceptions; - using WorkflowConsoleApp.Activities; - using WorkflowConsoleApp.Models; +using System.Threading.Tasks; +using Dapr.Workflow; +using DurableTask.Core.Exceptions; +using WorkflowConsoleApp.Activities; +using WorkflowConsoleApp.Models; +namespace WorkflowConsoleApp.Workflows +{ class OrderProcessingWorkflow : Workflow { public override async Task RunAsync(WorkflowContext context, OrderPayload order)