diff --git a/src/core/Elsa.Core/Activities/Container.cs b/src/core/Elsa.Core/Activities/Container.cs index df84ad7ca..bdefba3ce 100644 --- a/src/core/Elsa.Core/Activities/Container.cs +++ b/src/core/Elsa.Core/Activities/Container.cs @@ -36,6 +36,9 @@ public abstract class Container : Activity, IContainer // Schedule children. await ScheduleChildrenAsync(context); + + // Because we override ExecuteAsync, this activity will not automatically complete. + // Instead, it's up to the derived classes to explicitly complete this activity. } protected virtual async ValueTask OnChildActivityCompletedAsync(ActivityCompleted signal, SignalContext context) diff --git a/src/core/Elsa.Core/Activities/Sequence.cs b/src/core/Elsa.Core/Activities/Sequence.cs index 4c39ebc08..71d36fe42 100644 --- a/src/core/Elsa.Core/Activities/Sequence.cs +++ b/src/core/Elsa.Core/Activities/Sequence.cs @@ -2,7 +2,6 @@ using System.ComponentModel; using Elsa.Attributes; using Elsa.Contracts; using Elsa.Models; -using Elsa.Signals; namespace Elsa.Activities; @@ -36,7 +35,7 @@ public class Sequence : Container if (currentIndex >= childActivities.Count) { - await context.SignalAsync(new ActivityCompleted()); + await context.CompleteActivityAsync(); return; } diff --git a/src/core/Elsa.Core/Models/Activity.cs b/src/core/Elsa.Core/Models/Activity.cs index db3d520c5..d6af43c7c 100644 --- a/src/core/Elsa.Core/Models/Activity.cs +++ b/src/core/Elsa.Core/Models/Activity.cs @@ -20,13 +20,7 @@ public abstract class Activity : ISignalHandler protected virtual async ValueTask ExecuteAsync(ActivityExecutionContext context) { Execute(context); - await OnExecutedAsync(context); - } - - protected virtual async ValueTask OnExecutedAsync(ActivityExecutionContext context) - { - // By default, signal that the activity is completed. - await context.SignalAsync(new ActivityCompleted()); + await CompleteAsync(context); } protected virtual ValueTask OnSignalReceivedAsync(object signal, SignalContext context) @@ -43,9 +37,17 @@ public abstract class Activity : ISignalHandler { } + /// + /// Notify the sytem that this activity completed. + /// + protected async ValueTask CompleteAsync(ActivityExecutionContext context) + { + await context.CompleteActivityAsync(); + } + protected void OnSignalReceived(Type signalType, Func handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler)); protected void OnSignalReceived(Func handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context)); - + protected void OnSignalReceived(Action handler) { OnSignalReceived((signal, context) => @@ -55,8 +57,11 @@ public abstract class Activity : ISignalHandler }); } - ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) => ExecuteAsync(context); - + async ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) + { + await ExecuteAsync(context); + } + async ValueTask ISignalHandler.HandleSignalAsync(object signal, SignalContext context) { // Give derived activity a chance to do something with the signal. diff --git a/src/core/Elsa.Core/Models/ActivityExecutionContext.cs b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs index fd558cba5..01bf81222 100644 --- a/src/core/Elsa.Core/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Core/Models/ActivityExecutionContext.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.Reflection; using Elsa.Activities; using Elsa.Contracts; +using Elsa.Signals; namespace Elsa.Models; @@ -190,6 +191,14 @@ public class ActivityExecutionContext return; } } + + /// + /// Explicitly complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + public async ValueTask CompleteActivityAsync() + { + await SignalAsync(new ActivityCompleted()); + } /// /// Returns a flattened list of the current context's ancestors. diff --git a/test/Elsa.IntegrationTests/Workflows/ForEachWorkflowTests.cs b/test/Elsa.IntegrationTests/Workflows/ForEachWorkflowTests.cs index 9f5ebf57d..d924a4794 100644 --- a/test/Elsa.IntegrationTests/Workflows/ForEachWorkflowTests.cs +++ b/test/Elsa.IntegrationTests/Workflows/ForEachWorkflowTests.cs @@ -55,7 +55,11 @@ public class ForEachWorkflowTests { Items = new Input>(_items), CurrentValue = currentItem, - Body = new WriteLine(context => currentItem.Get(context)) + Body = new If(() => true) + { + Then = new Break(), + Else = new WriteLine() + } }, } }); diff --git a/test/Elsa.IntegrationTests/Workflows/SequentialWorkflowTests.cs b/test/Elsa.IntegrationTests/Workflows/SequentialWorkflowTests.cs index 0738a8f01..0997677e8 100644 --- a/test/Elsa.IntegrationTests/Workflows/SequentialWorkflowTests.cs +++ b/test/Elsa.IntegrationTests/Workflows/SequentialWorkflowTests.cs @@ -16,24 +16,48 @@ public class SequentialWorkflowTests { private readonly IWorkflowRunner _workflowRunner; private readonly CapturingTextWriter _capturingTextWriter = new(); - private readonly Workflow _workflow; public SequentialWorkflowTests(ITestOutputHelper testOutputHelper) { var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); _workflowRunner = services.GetRequiredService(); - _workflow = new WorkflowDefinitionBuilder().BuildWorkflow(new SequentialWorkflow()); } - - [Fact(DisplayName = "Sequence completes only after its child activities complete")] + + [Fact(DisplayName = "Sequence completes after its child activities complete")] public async Task Test1() { - await _workflowRunner.RunAsync(_workflow); + var workflow = new WorkflowDefinitionBuilder().BuildWorkflow(new SequentialWorkflow()); + await _workflowRunner.RunAsync(workflow); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Line 1", "Line 2", "Line 3" }, lines); + } + + [Fact(DisplayName = "Sequence completes after its child sequence activity complete")] + public async Task Test2() + { + var workflow = new WorkflowDefinitionBuilder().BuildWorkflow(new NestedSequentialWorkflow()); + await _workflowRunner.RunAsync(workflow); var lines = _capturingTextWriter.Lines.ToList(); Assert.Equal(new[] { "Start", "Line 1", "Line 2", "Line 3", "End" }, lines); } - + private class SequentialWorkflow : IWorkflow + { + public void Build(IWorkflowDefinitionBuilder workflow) + { + workflow.WithRoot(new Sequence + { + Activities = + { + new WriteLine("Line 1"), + new WriteLine("Line 2"), + new WriteLine("Line 3") + } + }); + } + } + + private class NestedSequentialWorkflow : IWorkflow { public void Build(IWorkflowDefinitionBuilder workflow) {