diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs index 9ec952fa8..199885071 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs @@ -5,6 +5,7 @@ using Elsa.Workflows.Core.Activities.Flowchart.Attributes; using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; +using JetBrains.Annotations; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; @@ -13,12 +14,25 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; /// [FlowNode("True", "False")] [Activity("Elsa", "Flow", "Evaluate a Boolean condition to determine which path to execute next.")] +[PublicAPI] public class FlowDecision : Activity { /// public FlowDecision([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } + + /// + public FlowDecision(Func condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) + { + Condition = new(condition); + } + + /// + public FlowDecision(Func> condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) + { + Condition = new(condition); + } /// /// The condition to evaluate. diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index afeaf953f..58c102729 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -38,7 +38,7 @@ public class Flowchart : Container { var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId; var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : default; - var startActivity = triggerActivity ?? Start; + var startActivity = triggerActivity ?? Start ?? Activities.FirstOrDefault(); if (startActivity == null!) { @@ -101,7 +101,7 @@ public class Flowchart : Container var executionCount = scope.GetExecutionCount(activity); var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount); - if (haveInboundActivitiesExecuted) + if (haveInboundActivitiesExecuted) await flowchartActivityExecutionContext.ScheduleActivityAsync(activity); } else @@ -114,7 +114,7 @@ public class Flowchart : Container if (!children.Any()) { // If there are no more pending activities, mark this activity as completed. - var hasPendingChildren = scope.HasPendingActivities(); + var hasPendingChildren = !HaveAllLeafsExecuted(scope); if (!hasPendingChildren) await flowchartActivityExecutionContext.CompleteActivityAsync(); @@ -123,4 +123,11 @@ public class Flowchart : Container flowchartActivityExecutionContext.SetProperty(ScopeProperty, scope); context.StopPropagation(); } + + private bool HaveAllLeafsExecuted(FlowScope scope) + { + var leafs = Activities.Where(x => Connections.All(y => y.Source != x)).ToList(); + var leafsExecuted = leafs.All(x => scope.GetExecutionCount(x) > 0); + return leafsExecuted; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs index f09b4c7f7..6690ef395 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs @@ -72,4 +72,6 @@ internal class FlowScope } public long GetExecutionCount(IActivity activity) => Activities.ContainsKey(activity.Id) ? Activities[activity.Id].ExecutionCount : 0; + + public void Clear() => Activities.Clear(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 7f480bb41..c9c03811c 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -105,4 +105,10 @@ public static class WorkflowExecutionContextExtensions workflowExecutionContext.Scheduler.Schedule(workItem); workflowExecutionContext.AddCompletionCallback(owner, activityNode, completionCallback); } + + /// + /// Returns true if all activities have completed. + /// + /// True if all activities have completed, otherwise false. + public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => !workflowExecutionContext.ActivityExecutionContexts.Any(); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs index 13573a919..9745801b0 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs @@ -1,3 +1,4 @@ +using Elsa.Extensions; using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Pipelines.WorkflowExecution; @@ -46,8 +47,8 @@ public class DefaultActivitySchedulerMiddleware : WorkflowExecutionMiddleware // Invoke next middleware. await Next(context); - // If there are no bookmarks and all activities are completed, complete the workflow. + // If all activities are completed, complete the workflow. if (context.Status == WorkflowStatus.Running) - context.TransitionTo(!context.Bookmarks.Any() ? WorkflowSubStatus.Finished : WorkflowSubStatus.Suspended); + context.TransitionTo(context.AllActivitiesCompleted() ? WorkflowSubStatus.Finished : WorkflowSubStatus.Suspended); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs index 39ad12bd2..da2943b29 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs @@ -246,6 +246,12 @@ public class ActivityExecutionContext : IExecutionContext Properties[key] = value; return value; } + + /// + /// Removes a property associated with the current activity context. + /// + /// The property key. + public void RemoveProperty(string key) => Properties.Remove(key); public T GetRequiredService() where T : notnull => WorkflowExecutionContext.GetRequiredService(); public object GetRequiredService(Type serviceType) => WorkflowExecutionContext.GetRequiredService(serviceType); diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/BraidedWorkflowTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/BraidedWorkflowTests.cs new file mode 100644 index 000000000..8df20997d --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/BraidedWorkflowTests.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading.Tasks; +using Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows; +using Elsa.Testing.Shared; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins; + +public class BraidedWorkflowTests +{ + private readonly IWorkflowRunner _workflowRunner; + private readonly CapturingTextWriter _capturingTextWriter = new(); + + public BraidedWorkflowTests(ITestOutputHelper testOutputHelper) + { + var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); + _workflowRunner = services.GetRequiredService(); + } + + [Fact(DisplayName = "Braided workflows are executed correctly")] + public async Task Test1() + { + await _workflowRunner.RunAsync(); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "WriteLine1", "WriteLine2", "WriteLine3", "WriteLine4", "WriteLine5", "WriteLine6", "WriteLine7" }, lines); + } + + [Fact(DisplayName = "Braided workflows complete the workflow")] + public async Task Test2() + { + var result = await _workflowRunner.RunAsync(); + Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/ImplicitLoopWorkflowTests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/ImplicitLoopWorkflowTests.cs new file mode 100644 index 000000000..a1c632e71 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/ImplicitLoopWorkflowTests.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Threading.Tasks; +using Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows; +using Elsa.Testing.Shared; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins; + +public class ImplicitWorkflowTests +{ + private readonly IWorkflowRunner _workflowRunner; + private readonly CapturingTextWriter _capturingTextWriter = new(); + + public ImplicitWorkflowTests(ITestOutputHelper testOutputHelper) + { + var services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); + _workflowRunner = services.GetRequiredService(); + } + + [Fact(DisplayName = "Implicit loop workflows are executed correctly")] + public async Task Test1() + { + await _workflowRunner.RunAsync(); + var lines = _capturingTextWriter.Lines.ToList(); + Assert.Equal(new[] { "Start", "Retry", "End" }, lines); + } + + [Fact(DisplayName = "Implicit loop workflows complete the workflow")] + public async Task Test2() + { + var result = await _workflowRunner.RunAsync(); + Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/BraidedWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/BraidedWorkflow.cs new file mode 100644 index 000000000..24ff97854 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/BraidedWorkflow.cs @@ -0,0 +1,53 @@ +using Elsa.Workflows.Core.Abstractions; +using Elsa.Workflows.Core.Activities; +using Elsa.Workflows.Core.Activities.Flowchart.Activities; +using Elsa.Workflows.Core.Activities.Flowchart.Models; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows; + +public class BraidedWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder workflow) + { + var writeLine1 = new WriteLine("WriteLine1"); + var writeLine2 = new WriteLine("WriteLine2"); + var writeLine3 = new WriteLine("WriteLine3"); + var writeLine4 = new WriteLine("WriteLine4"); + var writeLine5 = new WriteLine("WriteLine5"); + var writeLine6 = new WriteLine("WriteLine6"); + var writeLine7 = new WriteLine("WriteLine7"); + + workflow.Root = new Flowchart + { + Start = writeLine1, + + Activities = + { + writeLine1, + writeLine2, + writeLine3, + writeLine4, + writeLine5, + writeLine6, + writeLine7, + }, + + Connections = + { + new Connection(writeLine1, writeLine2), + new Connection(writeLine1, writeLine3), + new Connection(writeLine1, writeLine4), + + new Connection(writeLine2, writeLine5), + new Connection(writeLine3, writeLine5), + + new Connection(writeLine3, writeLine6), + new Connection(writeLine4, writeLine6), + + new Connection(writeLine5, writeLine7), + new Connection(writeLine6, writeLine7), + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs new file mode 100644 index 000000000..ff3da9c6c --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/ImplicitJoins/Workflows/ImplicitLoopWorkflow.cs @@ -0,0 +1,43 @@ +using Elsa.Workflows.Core.Abstractions; +using Elsa.Workflows.Core.Activities; +using Elsa.Workflows.Core.Activities.Flowchart.Activities; +using Elsa.Workflows.Core.Activities.Flowchart.Models; +using Elsa.Workflows.Core.Contracts; + +namespace Elsa.IntegrationTests.Scenarios.ImplicitJoins.Workflows; + +public class ImplicitLoopWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var counterVariable = builder.WithVariable(0); + var start = new WriteLine("Start"); + var incrementCounter = new SetVariable(counterVariable, context => counterVariable.Get(context) + 1); + var counterGreaterThanOne = new FlowDecision(context => counterVariable.Get(context) > 1); + var retry = new WriteLine("Retry"); + var end = new WriteLine("End"); + + builder.Root = new Flowchart + { + Start = start, + + Activities = + { + start, + incrementCounter, + counterGreaterThanOne, + retry, + end + }, + + Connections = + { + new Connection(start, incrementCounter), + new Connection(incrementCounter, counterGreaterThanOne), + new Connection(counterGreaterThanOne, retry, SourcePort: "False"), + new Connection(counterGreaterThanOne, end, SourcePort: "True"), + new Connection(retry, incrementCounter), + } + }; + } +} \ No newline at end of file