Add support for implicit looping constructs
Fixes the issue where a workflow would not complete, even though all flow chart activities executed
This commit is contained in:
parent
53cb1cc63c
commit
5331476081
|
|
@ -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;
|
|||
/// </summary>
|
||||
[FlowNode("True", "False")]
|
||||
[Activity("Elsa", "Flow", "Evaluate a Boolean condition to determine which path to execute next.")]
|
||||
[PublicAPI]
|
||||
public class FlowDecision : Activity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public FlowDecision([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FlowDecision(Func<ExpressionExecutionContext, bool> condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
|
||||
{
|
||||
Condition = new(condition);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public FlowDecision(Func<ExpressionExecutionContext, ValueTask<bool>> condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
|
||||
{
|
||||
Condition = new(condition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The condition to evaluate.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -105,4 +105,10 @@ public static class WorkflowExecutionContextExtensions
|
|||
workflowExecutionContext.Scheduler.Schedule(workItem);
|
||||
workflowExecutionContext.AddCompletionCallback(owner, activityNode, completionCallback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if all activities have completed.
|
||||
/// </summary>
|
||||
/// <returns>True if all activities have completed, otherwise false.</returns>
|
||||
public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => !workflowExecutionContext.ActivityExecutionContexts.Any();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +246,12 @@ public class ActivityExecutionContext : IExecutionContext
|
|||
Properties[key] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property associated with the current activity context.
|
||||
/// </summary>
|
||||
/// <param name="key">The property key.</param>
|
||||
public void RemoveProperty(string key) => Properties.Remove(key);
|
||||
|
||||
public T GetRequiredService<T>() where T : notnull => WorkflowExecutionContext.GetRequiredService<T>();
|
||||
public object GetRequiredService(Type serviceType) => WorkflowExecutionContext.GetRequiredService(serviceType);
|
||||
|
|
|
|||
|
|
@ -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<IWorkflowRunner>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Braided workflows are executed correctly")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _workflowRunner.RunAsync<BraidedWorkflow>();
|
||||
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<BraidedWorkflow>();
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IWorkflowRunner>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Implicit loop workflows are executed correctly")]
|
||||
public async Task Test1()
|
||||
{
|
||||
await _workflowRunner.RunAsync<ImplicitLoopWorkflow>();
|
||||
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<ImplicitLoopWorkflow>();
|
||||
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<int>(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),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue