From bddf522e559487d1b4dd4d0a27ea646092d78bd1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 7 Oct 2023 14:22:51 +0200 Subject: [PATCH] Activity Preconditions (#4507) * Add CanExecuteAsync precondition method * Rename GetInput to GetWorkflowInput to disambiguate between input sent to workflow and input sent to indiviudal activities. --- .../Activities/MessageReceived.cs | 4 +- .../Elsa.Http/Activities/HttpEndpoint.cs | 2 +- .../Activities/MessageReceived.cs | 2 +- .../Elsa.Scheduling/Activities/StartAt.cs | 2 +- .../Elsa.Telnyx/Activities/AnswerCallBase.cs | 2 +- .../Elsa.Telnyx/Activities/BridgeCallsBase.cs | 2 +- .../Elsa.Telnyx/Activities/CallAnswered.cs | 2 +- .../Elsa.Telnyx/Activities/CallHangup.cs | 2 +- .../Elsa.Telnyx/Activities/DialAndWait.cs | 4 +- .../Activities/GatherUsingAudio.cs | 2 +- .../Activities/GatherUsingSpeak.cs | 2 +- .../Elsa.Telnyx/Activities/IncomingCall.cs | 2 +- .../Activities/StartRecordingBase.cs | 2 +- .../Elsa.Telnyx/Activities/TransferCall.cs | 6 +-- .../Elsa.Telnyx/Activities/WebhookEvent.cs | 2 +- .../Abstractions/Activity.cs | 22 +++++++++ .../Contexts/ActivityExecutionContext.cs | 3 +- .../Contexts/WorkflowExecutionContext.cs | 10 ++-- .../Contracts/IActivity.cs | 5 ++ .../Enums/ActivityStatus.cs | 5 ++ .../ActivityExecutionContextExtensions.cs | 6 +-- .../ExpressionExecutionContextExtensions.cs | 10 ++++ .../DefaultActivityInvokerMiddleware.cs | 10 ++++ .../Activities/RunTask.cs | 2 +- .../CanExecute/Activities/CustomActivity.cs | 37 ++++++++++++++ .../Scenarios/CanExecute/Tests.cs | 49 +++++++++++++++++++ .../CanExecute/Workflows/MagicWorkflow.cs | 26 ++++++++++ .../Activities/CustomActivity.cs | 2 +- 28 files changed, 196 insertions(+), 29 deletions(-) create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Activities/CustomActivity.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Tests.cs create mode 100644 test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Workflows/MagicWorkflow.cs diff --git a/src/modules/Elsa.AzureServiceBus/Activities/MessageReceived.cs b/src/modules/Elsa.AzureServiceBus/Activities/MessageReceived.cs index f33ff459e..e38256029 100644 --- a/src/modules/Elsa.AzureServiceBus/Activities/MessageReceived.cs +++ b/src/modules/Elsa.AzureServiceBus/Activities/MessageReceived.cs @@ -88,7 +88,7 @@ public class MessageReceived : Trigger protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // If we did not receive external input, it means we are just now encountering this activity. - if (!context.TryGetInput(InputKey, out var receivedMessage)) + if (!context.TryGetWorkflowInput(InputKey, out var receivedMessage)) { // Create bookmarks for when we receive the expected HTTP request. context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext), Resume); @@ -102,7 +102,7 @@ public class MessageReceived : Trigger private async ValueTask Resume(ActivityExecutionContext context) { - var receivedMessage = context.GetInput(InputKey); + var receivedMessage = context.GetWorkflowInput(InputKey); await SetResultAsync(receivedMessage, context); await context.CompleteActivityAsync(); } diff --git a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs index c6b3655e5..90d84b585 100644 --- a/src/modules/Elsa.Http/Activities/HttpEndpoint.cs +++ b/src/modules/Elsa.Http/Activities/HttpEndpoint.cs @@ -198,7 +198,7 @@ public class HttpEndpoint : Trigger context.Set(Result, request); // Read route data, if any. - var path = context.GetInput(RequestPathInputKey); + var path = context.GetWorkflowInput(RequestPathInputKey); var routeData = GetRouteData(httpContext, path); var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!); diff --git a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs index 1dbcfa74a..c63dd13cd 100644 --- a/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs +++ b/src/modules/Elsa.MassTransit/Activities/MessageReceived.cs @@ -32,7 +32,7 @@ public class MessageReceived : Trigger protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // If we did not receive external input, it means we are just now encountering this activity and we need to block execution by creating a bookmark. - if (!context.TryGetInput(InputKey, out var message)) + if (!context.TryGetWorkflowInput(InputKey, out var message)) { // Create bookmarks for when we receive the expected HTTP request. context.CreateBookmark(GetBookmarkPayload(context.ExpressionExecutionContext)); diff --git a/src/modules/Elsa.Scheduling/Activities/StartAt.cs b/src/modules/Elsa.Scheduling/Activities/StartAt.cs index 202c2bf8d..5d0194f66 100644 --- a/src/modules/Elsa.Scheduling/Activities/StartAt.cs +++ b/src/modules/Elsa.Scheduling/Activities/StartAt.cs @@ -76,7 +76,7 @@ public class StartAt : Trigger protected override void Execute(ActivityExecutionContext context) { // If external input was received, it means this activity got triggered and does not need to create a bookmark. - if (context.TryGetInput(InputKey, out _)) + if (context.TryGetWorkflowInput(InputKey, out _)) return; // No external input received, so create a bookmark. diff --git a/src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs b/src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs index 2457464d9..e8aad2be6 100644 --- a/src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs +++ b/src/modules/Elsa.Telnyx/Activities/AnswerCallBase.cs @@ -67,7 +67,7 @@ public abstract class AnswerCallBase : Activity private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); context.Set(Result, payload); await HandleConnectedAsync(context); } diff --git a/src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs b/src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs index 89bd6c841..8a140ce0a 100644 --- a/src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs +++ b/src/modules/Elsa.Telnyx/Activities/BridgeCallsBase.cs @@ -79,7 +79,7 @@ public abstract class BridgeCallsBase : Activity private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var payload = context.GetInput()!; + var payload = context.GetWorkflowInput()!; var callControlIdA = CallControlIdA.Get(context); var callControlIdB = CallControlIdB.Get(context);; diff --git a/src/modules/Elsa.Telnyx/Activities/CallAnswered.cs b/src/modules/Elsa.Telnyx/Activities/CallAnswered.cs index e0267c8cc..6940d30b2 100644 --- a/src/modules/Elsa.Telnyx/Activities/CallAnswered.cs +++ b/src/modules/Elsa.Telnyx/Activities/CallAnswered.cs @@ -40,7 +40,7 @@ public class CallAnswered : Activity private async ValueTask Resume(ActivityExecutionContext context) { - var input = context.GetInput(WebhookSerializerOptions.Create()); + var input = context.GetWorkflowInput(WebhookSerializerOptions.Create()); context.Set(Result, input); await context.CompleteActivityAsync(); } diff --git a/src/modules/Elsa.Telnyx/Activities/CallHangup.cs b/src/modules/Elsa.Telnyx/Activities/CallHangup.cs index fd644b226..5131dc560 100644 --- a/src/modules/Elsa.Telnyx/Activities/CallHangup.cs +++ b/src/modules/Elsa.Telnyx/Activities/CallHangup.cs @@ -40,7 +40,7 @@ public class CallHangup : Activity private async ValueTask Resume(ActivityExecutionContext context) { - var input = context.GetInput(WebhookSerializerOptions.Create()); + var input = context.GetWorkflowInput(WebhookSerializerOptions.Create()); context.Set(Result, input); await context.CompleteActivityAsync(); } diff --git a/src/modules/Elsa.Telnyx/Activities/DialAndWait.cs b/src/modules/Elsa.Telnyx/Activities/DialAndWait.cs index 278ee3651..ba19878f3 100644 --- a/src/modules/Elsa.Telnyx/Activities/DialAndWait.cs +++ b/src/modules/Elsa.Telnyx/Activities/DialAndWait.cs @@ -89,14 +89,14 @@ public class DialAndWait : Activity private async ValueTask OnCallAnswered(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); Result.Set(context, payload); await context.CompleteActivityWithOutcomesAsync("Answered"); } private async ValueTask OnCallHangup(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); Result.Set(context, payload); await context.CompleteActivityWithOutcomesAsync("Hangup"); } diff --git a/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs b/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs index 611f2af86..3c1f3990a 100644 --- a/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs @@ -150,7 +150,7 @@ public class GatherUsingAudio : Activity, IBookmarksPers private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input"; context.Set(Result, payload); await context.CompleteActivityWithOutcomesAsync(outcome); diff --git a/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs b/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs index 26e9555ef..6d1a16807 100644 --- a/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs +++ b/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs @@ -181,7 +181,7 @@ public class GatherUsingSpeak : Activity private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); var outcome = payload.Status == "valid" ? "Valid input" : "Invalid input"; context.Set(Result, payload); await context.CompleteActivityWithOutcomesAsync(outcome); diff --git a/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs b/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs index 465f76e31..352220923 100644 --- a/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs @@ -63,7 +63,7 @@ public class IncomingCall : Trigger private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var webhookModel = context.GetInput(WebhookSerializerOptions.Create()); + var webhookModel = context.GetWorkflowInput(WebhookSerializerOptions.Create()); var callInitiatedPayload = (CallInitiatedPayload)webhookModel.Data.Payload; // Store webhook payload as output. diff --git a/src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs b/src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs index 147b83810..78af0eab1 100644 --- a/src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs +++ b/src/modules/Elsa.Telnyx/Activities/StartRecordingBase.cs @@ -101,7 +101,7 @@ public abstract class StartRecordingBase : Activity private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); context.Set(Result, payload); await HandleCallRecordingSavedAsync(context); } diff --git a/src/modules/Elsa.Telnyx/Activities/TransferCall.cs b/src/modules/Elsa.Telnyx/Activities/TransferCall.cs index 69117d785..6fa2b4c08 100644 --- a/src/modules/Elsa.Telnyx/Activities/TransferCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/TransferCall.cs @@ -105,7 +105,7 @@ public class TransferCall : Activity private ValueTask InitiatedAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); var callControlId = payload.CallControlId; var answeredBookmark = new WebhookEventBookmarkPayload(WebhookEventTypes.CallAnswered, callControlId); var hangupBookmark = new WebhookEventBookmarkPayload(WebhookEventTypes.CallHangup, callControlId); @@ -116,14 +116,14 @@ public class TransferCall : Activity private async ValueTask AnsweredAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); Result.Set(context, payload); await context.CompleteActivityWithOutcomesAsync("Transferred"); } private async ValueTask HangupAsync(ActivityExecutionContext context) { - var payload = context.GetInput(); + var payload = context.GetWorkflowInput(); Result.Set(context, payload); await context.CompleteActivityWithOutcomesAsync("Hangup"); } diff --git a/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs b/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs index 683556045..03fadd483 100644 --- a/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs +++ b/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs @@ -54,7 +54,7 @@ public class WebhookEvent : Activity private async ValueTask Resume(ActivityExecutionContext context) { - var input = context.GetInput(WebhookSerializerOptions.Create()); + var input = context.GetWorkflowInput(WebhookSerializerOptions.Create()); context.Set(Result, input.Data.Payload); await CompleteAsync(context); } diff --git a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs index 42f3d4c5f..448e4c073 100644 --- a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs @@ -88,6 +88,23 @@ public abstract class Activity : IActivity, ISignalHandler [JsonIgnore] public ICollection Behaviors { get; } = new List(); + /// + /// Override this method to return a value indicating whether the activity can execute. + /// + protected virtual ValueTask CanExecuteAsync(ActivityExecutionContext context) + { + var result = CanExecute(context); + return new(result); + } + + /// + /// Override this method to return a value indicating whether the activity can execute. + /// + protected virtual bool CanExecute(ActivityExecutionContext context) + { + return true; + } + /// /// Override this method to implement activity-specific logic. /// @@ -187,6 +204,11 @@ public abstract class Activity : IActivity, ISignalHandler { await context.CompleteActivityAsync(); } + + async ValueTask IActivity.CanExecuteAsync(ActivityExecutionContext context) + { + return await CanExecuteAsync(context); + } async ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) { diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index a7e7cc1c2..be888d22a 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -42,6 +42,7 @@ public class ActivityExecutionContext : IExecutionContext Activity = activity; ActivityDescriptor = activityDescriptor; StartedAt = startedAt; + Status = ActivityStatus.Pending; Tag = tag; CancellationToken = cancellationToken; Id = Guid.NewGuid().ToString(); @@ -140,7 +141,7 @@ public class ActivityExecutionContext : IExecutionContext /// /// Returns the metadata about the current activity. /// - public ActivityNode? ActivityNode => WorkflowExecutionContext.FindNodeByActivity(Activity); + public ActivityNode ActivityNode => WorkflowExecutionContext.FindNodeByActivity(Activity)!; /// /// Returns the global node ID for the current activity within the graph. diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 2f64b954b..ab0badbc5 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -157,7 +157,7 @@ public class WorkflowExecutionContext : IExecutionContext workflowExecutionContext.MemoryRegister = workflow.CreateRegister(); workflowExecutionContext.ExpressionExecutionContext = new ExpressionExecutionContext(serviceProvider, workflowExecutionContext.MemoryRegister, cancellationToken: cancellationTokens.ApplicationCancellationToken); - + await workflowExecutionContext.SetWorkflowAsync(workflow); return workflowExecutionContext; } @@ -339,7 +339,9 @@ public class WorkflowExecutionContext : IExecutionContext /// /// A list of s that are currently active. /// - public IReadOnlyCollection ActiveActivityExecutionContexts => ActivityExecutionContexts.Where(x => !x.IsCompleted || x.ParentActivityExecutionContext == null).ToList(); + public IReadOnlyCollection ActiveActivityExecutionContexts => ActivityExecutionContexts + .Where(x => !x.IsCompleted || x.ParentActivityExecutionContext == null && x.Status != ActivityStatus.Pending) + .ToList(); /// /// A list of s that are currently active. @@ -363,7 +365,7 @@ public class WorkflowExecutionContext : IExecutionContext /// /// The expression execution context for the current workflow execution. /// - public ExpressionExecutionContext? ExpressionExecutionContext { get; private set; } = default!; + public ExpressionExecutionContext? ExpressionExecutionContext { get; private set; } /// public IEnumerable Variables => Workflow.Variables; @@ -450,7 +452,7 @@ public class WorkflowExecutionContext : IExecutionContext /// Returns the containing the specified activity from the workflow graph. /// public ActivityNode? FindNodeByActivity(IActivity activity) => NodeActivityLookup[activity]; - + /// /// Returns the associated with the specified activity ID. /// diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivity.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivity.cs index dd0855449..f4afc8ec0 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivity.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivity.cs @@ -43,6 +43,11 @@ public interface IActivity /// IDictionary Metadata { get; set; } + /// + /// Returns a value indicating whether the activity can execute. + /// + ValueTask CanExecuteAsync(ActivityExecutionContext context); + /// /// Invoked when the activity executes. /// diff --git a/src/modules/Elsa.Workflows.Core/Enums/ActivityStatus.cs b/src/modules/Elsa.Workflows.Core/Enums/ActivityStatus.cs index aba8c993a..9c69c4579 100644 --- a/src/modules/Elsa.Workflows.Core/Enums/ActivityStatus.cs +++ b/src/modules/Elsa.Workflows.Core/Enums/ActivityStatus.cs @@ -5,6 +5,11 @@ namespace Elsa.Workflows.Core; /// public enum ActivityStatus { + /// + /// The activity is in the Pending state. + /// + Pending, + /// /// The activity is in the Running state. Note that event if an activity is running, it may not be executing. /// diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index defeb8114..35ccdd561 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -29,7 +29,7 @@ public static class ActivityExecutionContextExtensions /// /// Attempts to get a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static bool TryGetInput(this ActivityExecutionContext context, string key, out T value, JsonSerializerOptions? serializerOptions = default) + public static bool TryGetWorkflowInput(this ActivityExecutionContext context, string key, out T value, JsonSerializerOptions? serializerOptions = default) { var wellKnownTypeRegistry = context.GetRequiredService(); @@ -46,12 +46,12 @@ public static class ActivityExecutionContextExtensions /// /// Gets a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static T GetInput(this ActivityExecutionContext context, JsonSerializerOptions? serializerOptions = default) => context.GetInput(typeof(T).Name, serializerOptions); + public static T GetWorkflowInput(this ActivityExecutionContext context, JsonSerializerOptions? serializerOptions = default) => context.GetWorkflowInput(typeof(T).Name, serializerOptions); /// /// Gets a value from the input provided via . If a value was found, an attempt is made to convert it into the specified type T. /// - public static T GetInput(this ActivityExecutionContext context, string key, JsonSerializerOptions? serializerOptions = default) + public static T GetWorkflowInput(this ActivityExecutionContext context, string key, JsonSerializerOptions? serializerOptions = default) { var wellKnownTypeRegistry = context.GetRequiredService(); return context.WorkflowInput[key].ConvertTo(new ObjectConverterOptions(serializerOptions, wellKnownTypeRegistry))!; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index 2fca7c271..59ae9e700 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -72,6 +72,16 @@ public static class ExpressionExecutionContextExtensions /// public static IDictionary GetInput(this ExpressionExecutionContext context) => (IDictionary)context.TransientProperties[InputKey]; + /// + /// Returns input sent to the workflow. + /// + public static T GetWorkflowInput(this ExpressionExecutionContext context, string key) => context.GetActivityExecutionContext().GetWorkflowInput(key); + + /// + /// Returns input sent to the workflow. + /// + public static T GetWorkflowInput(this ExpressionExecutionContext context) => context.GetActivityExecutionContext().GetWorkflowInput(); + /// /// Returns the value of the specified input. /// diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 197477068..4b7b5cf6f 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -42,6 +42,16 @@ public class DefaultActivityInvokerMiddleware : IActivityExecutionMiddleware // Evaluate input properties. await EvaluateInputPropertiesAsync(context); + // Check if the activity can be executed. + if (!await context.Activity.CanExecuteAsync(context)) + { + context.Status = ActivityStatus.Pending; + context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time"); + return; + } + + context.Status = ActivityStatus.Running; + // Execute activity. await ExecuteActivityAsync(context); diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/RunTask.cs b/src/modules/Elsa.Workflows.Runtime/Activities/RunTask.cs index a0078298c..c8e061d8f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/RunTask.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/RunTask.cs @@ -94,7 +94,7 @@ public class RunTask : Activity, IBookmarksPersistedHandler private async ValueTask ResumeAsync(ActivityExecutionContext context) { - var input = context.GetInput(InputKey); + var input = context.GetWorkflowInput(InputKey); context.Set(Result, input); await context.CompleteActivityAsync(); } diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Activities/CustomActivity.cs b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Activities/CustomActivity.cs new file mode 100644 index 000000000..8ee81b73c --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Activities/CustomActivity.cs @@ -0,0 +1,37 @@ +using System; +using Elsa.Expressions.Models; +using Elsa.Extensions; +using Elsa.Workflows.Core; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Services; + +namespace Elsa.IntegrationTests.Scenarios.CanExecute.Activities; + +public class CustomActivity : CodeActivity +{ + public CustomActivity(int magicNumber) + { + MagicNumber = new (magicNumber); + } + + public CustomActivity(Func magicNumber) + { + MagicNumber = new (magicNumber); + } + + public Input MagicNumber { get; set; } + + protected override bool CanExecute(ActivityExecutionContext context) + { + var magicNumber = MagicNumber.Get(context); + return magicNumber == 42; + } + + protected override void Execute(ActivityExecutionContext context) + { + var provider = context.GetService() ?? new StandardOutStreamProvider(Console.Out); + var textWriter = provider.GetTextWriter(); + textWriter.WriteLine("Welcome to the world of Might and Magic!"); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Tests.cs new file mode 100644 index 000000000..c0b0373ac --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Tests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elsa.IntegrationTests.Scenarios.CanExecute.Activities; +using Elsa.IntegrationTests.Scenarios.CanExecute.Workflows; +using Elsa.Testing.Shared; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Options; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.CanExecute; + +public class CanExecuteTests +{ + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IServiceProvider _services; + private readonly IWorkflowRunner _workflowRunner; + + public CanExecuteTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .AddActivitiesFrom() + .Build(); + + _workflowRunner = _services.GetRequiredService(); + } + + [Theory(DisplayName = "Activities are executed only when they report that they can execute.")] + [InlineData(12, "Magic number is 12")] + [InlineData(42, "Magic number is 42\nWelcome to the world of Might and Magic!\nDone")] + public async Task Test1(int magicNumber, string expectedLines) + { + await _services.PopulateRegistriesAsync(); + var runOptions = new RunWorkflowOptions + { + Input = new Dictionary + { + ["MagicNumber"] = magicNumber + } + }; + await _workflowRunner.RunAsync(runOptions); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(expectedLines.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries), lines); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Workflows/MagicWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Workflows/MagicWorkflow.cs new file mode 100644 index 000000000..385ac6bb9 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/CanExecute/Workflows/MagicWorkflow.cs @@ -0,0 +1,26 @@ +using Elsa.Extensions; +using Elsa.IntegrationTests.Scenarios.CanExecute.Activities; +using Elsa.Workflows.Core; +using Elsa.Workflows.Core.Activities; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.IntegrationTests.Scenarios.CanExecute.Workflows; + +public class MagicWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var magicNumberVariable = builder.WithVariable(); + + builder.Root = new Sequence + { + Activities = + { + new SetVariable(magicNumberVariable, context => context.GetWorkflowInput("MagicNumber")), + new WriteLine(context => $"Magic number is {magicNumberVariable.Get(context)}"), + new CustomActivity(context => magicNumberVariable.Get(context)), + new WriteLine("Done") + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/FlowchartNextActivity/Activities/CustomActivity.cs b/test/integration/Elsa.IntegrationTests/Scenarios/FlowchartNextActivity/Activities/CustomActivity.cs index 12afb3243..bdb403a57 100644 --- a/test/integration/Elsa.IntegrationTests/Scenarios/FlowchartNextActivity/Activities/CustomActivity.cs +++ b/test/integration/Elsa.IntegrationTests/Scenarios/FlowchartNextActivity/Activities/CustomActivity.cs @@ -9,5 +9,5 @@ public class CustomActivity : CodeActivity protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { await context.CompleteActivityAsync(); - } + } } \ No newline at end of file