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