Change For/While/ForEach/IfElse behavior

Before this change, the aforementioned activities would always schedule the Done outcome.
After this change, the Done outcome is scheduled **after** the Iterate branch and True or False branches have completed
This commit is contained in:
Sipke Schoorstra 2021-01-12 09:28:48 +01:00
parent 997fb53b08
commit fcfe7f08b4
28 changed files with 190 additions and 75 deletions

View file

@ -8,12 +8,12 @@ namespace Elsa.Activities.Console
{
public static class WriteLineBuilderExtensions
{
public static IOutcomeBuilder WriteLine(this IBuilder builder, Action<ISetupActivity<WriteLine>> setup, string? name = default, string? id = default) => WriteLine(builder.Then(setup).WithName(name).WithId(id));
public static IOutcomeBuilder WriteLine(this IBuilder builder, Func<ActivityExecutionContext, string> text, string? name = default, string? id = default) => builder.WriteLine(activity => activity.Set(x => x.Text, text), name, id);
public static IOutcomeBuilder WriteLine(this IBuilder builder, Func<ActivityExecutionContext, ValueTask<string>> text, string? name = default, string? id = default) => builder.WriteLine(activity => activity.Set(x => x.Text, text!), name, id);
public static IOutcomeBuilder WriteLine(this IBuilder builder, Func<string> text, string? name = default, string? id = default) => builder.WriteLine(activity => activity.Set(x => x.Text, text), name, id);
public static IOutcomeBuilder WriteLine(this IBuilder builder, Func<ValueTask<string>> text, string? name = default, string? id = default) => builder.WriteLine(activity => activity.Set(x => x.Text, text!), name, id);
public static IOutcomeBuilder WriteLine(this IBuilder builder, string text, string? name = default, string? id = default) => builder.WriteLine(activity => activity.Set(x => x.Text, text), name, id);
private static IOutcomeBuilder WriteLine(IActivityBuilder writeLine) => writeLine.When(OutcomeNames.Done);
public static IActivityBuilder WriteLine(this IBuilder builder, Action<ISetupActivity<WriteLine>> setup) => builder.Then(setup);
public static IActivityBuilder WriteLine(this IBuilder builder, Func<ActivityExecutionContext, string> text) => builder.WriteLine(activity => activity.WithText(text));
public static IActivityBuilder WriteLine(this IBuilder builder, Func<ActivityExecutionContext, ValueTask<string>> text) => builder.WriteLine(activity => activity.WithText(text!));
public static IActivityBuilder WriteLine(this IBuilder builder, Func<string> text) => builder.WriteLine(activity => activity.WithText(text!));
public static IActivityBuilder WriteLine(this IBuilder builder, Func<ValueTask<string>> text) => builder.WriteLine(activity => activity.WithText(text!));
public static IActivityBuilder WriteLine(this IBuilder builder, string text) => builder.WriteLine(activity => activity.WithText(text!));
}
}

View file

@ -25,16 +25,40 @@ namespace Elsa.ActivityResults
{
var outcomes = activityExecutionContext.Outcomes = Outcomes.ToList();
var workflowExecutionContext = activityExecutionContext.WorkflowExecutionContext;
var nextConnections = GetNextConnections(workflowExecutionContext, activityExecutionContext.ActivityBlueprint.Id, outcomes).ToList();
var nextActivities = GetNextActivities(
workflowExecutionContext,
activityExecutionContext.ActivityBlueprint.Id,
outcomes).ToList();
var nextActivities =
(
from connection in nextConnections
from activityBlueprint in workflowExecutionContext.WorkflowBlueprint.Activities
where activityBlueprint.Id == connection.Target.Activity.Id
select activityBlueprint.Id
)
.Distinct();
workflowExecutionContext.ScheduleActivities(nextActivities, activityExecutionContext.Output);
foreach (var nextConnection in nextConnections)
workflowExecutionContext.ExecutionLog.Add(nextConnection);
workflowExecutionContext.ScheduleActivities(nextActivities, activityExecutionContext.Output);
}
private IEnumerable<string> GetNextActivities(
public static IEnumerable<string> GetNextActivities(
WorkflowExecutionContext workflowContext,
string sourceId,
IEnumerable<string> outcomes)
{
var nextConnections = GetNextConnections(workflowContext, sourceId, outcomes);
var query =
from connection in nextConnections
from activityBlueprint in workflowContext.WorkflowBlueprint.Activities
where activityBlueprint.Id == connection.Target.Activity.Id
select activityBlueprint.Id;
return query.Distinct();
}
public static IEnumerable<IConnection> GetNextConnections(
WorkflowExecutionContext workflowContext,
string sourceId,
IEnumerable<string> outcomes)
@ -47,12 +71,10 @@ namespace Elsa.ActivityResults
let connectionOutcome = connection.Source.Outcome ?? OutcomeNames.Done
let isConnectionOutcome = connectionOutcome.Equals(outcome.outcome, StringComparison.OrdinalIgnoreCase)
where connection.Source.Activity.Id == sourceId && isConnectionOutcome
from activityBlueprint in workflowContext.WorkflowBlueprint.Activities
where activityBlueprint.Id == connection.Target.Activity.Id
orderby outcome.order
select activityBlueprint.Id;
orderby outcome.order
select connection;
return query.Distinct();
return query;
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Elsa.Services;
using Elsa.Services.Models;
namespace Elsa.Builders
{
@ -28,5 +29,6 @@ namespace Elsa.Builders
IActivityBuilder LoadWorkflowContext(bool value = true);
IActivityBuilder SaveWorkflowContext(bool value = true);
IActivityBuilder PersistWorkflow(bool value = true);
IWorkflowBlueprint Build();
}
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Services.Models
{
public interface IBranchingActivity
{
void Unwind(ActivityExecutionContext context);
}
}

View file

@ -67,7 +67,7 @@ namespace Elsa.Services.Models
}
public bool DeleteCompletedInstances => WorkflowBlueprint.DeleteCompletedInstances;
public ICollection<string> ExecutionLog => new List<string>();
public IList<IConnection> ExecutionLog { get; } = new List<IConnection>();
public WorkflowStatus Status => WorkflowInstance.WorkflowStatus;
public bool HasBlockingActivities => WorkflowInstance.BlockingActivities.Any();
public object? WorkflowContext { get; set; }
@ -135,12 +135,6 @@ namespace Elsa.Services.Models
public void SetWorkflowContext(object? value) => WorkflowContext = value;
public T GetWorkflowContext<T>() => (T) WorkflowContext!;
public async ValueTask<IEnumerable<RuntimeActivityInstance>> ActivateActivitiesAsync(CancellationToken cancellationToken = default)
{
var activityExecutionContexts = WorkflowBlueprint.Activities.Select(x => new ActivityExecutionContext(ServiceScope, this, x, null, CancellationToken.None));
return await Task.WhenAll(activityExecutionContexts.Select(async x => await x.ActivateActivityAsync(cancellationToken)));
}
/// <summary>
/// Remove empty activity data to save on document size.
/// </summary>
@ -156,4 +150,6 @@ namespace Elsa.Services.Models
return activityBlueprint != null && activityBlueprint.PersistOutput;
}
}
public record ExecutionLogEntry(string ActivityId, string Outcome);
}

View file

@ -1,7 +1,6 @@
using System;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
@ -12,7 +11,7 @@ namespace Elsa.Activities.ControlFlow
Description = "Iterate between two numbers.",
Outcomes = new[] { OutcomeNames.Iterate, OutcomeNames.Done }
)]
public class For : Activity
public class For : IteratingActivity
{
[ActivityProperty(Hint = "An expression that evaluates to the starting number.")]
public long Start { get; set; }

View file

@ -14,7 +14,7 @@ namespace Elsa.Activities.ControlFlow
Description = "Iterate over a collection.",
Outcomes = new[] { OutcomeNames.Iterate, OutcomeNames.Done }
)]
public class ForEach : Activity
public class ForEach : IteratingActivity
{
[ActivityProperty(Hint = "Enter an expression that evaluates to a collection of items to iterate over.")]
public ICollection<object> Items { get; set; } = new Collection<object>();
@ -30,7 +30,7 @@ namespace Elsa.Activities.ControlFlow
get => GetState<int?>();
set => SetState(value);
}
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
var collection = ItemsCopy;

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Services;
@ -12,18 +14,58 @@ namespace Elsa.Activities.ControlFlow
Description = "Evaluate a Boolean expression and continue execution depending on the result.",
Outcomes = new[] { True, False, OutcomeNames.Done }
)]
public class IfElse : Activity
public class IfElse : Activity, IBranchingActivity
{
public const string True = "True";
public const string False = "False";
[ActivityProperty(Hint = "The condition to evaluate.")]
public bool Condition { get; set; }
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
var outcome = Condition ? True : False;
return Outcomes(OutcomeNames.Done, outcome);
return Outcome(outcome);
}
public void Unwind(ActivityExecutionContext context)
{
var workflowExecutionContext = context.WorkflowExecutionContext;
var workflowBlueprint = workflowExecutionContext.WorkflowBlueprint;
var activityExecutionContext = context;
var currentActivityBlueprint = activityExecutionContext.ActivityBlueprint;
var currentActivityId = currentActivityBlueprint.Id;
// An IfElse activity completed within a burst of execution, which means its outcome (true or false) did not yield child activities to execute (an empty branch).
// Schedule its child activities connected to the "Done" outcome.
if (currentActivityBlueprint.Type == nameof(IfElse))
{
var nextActivities = GetNextActivities(workflowExecutionContext, currentActivityId);
workflowExecutionContext.ScheduleActivities(nextActivities);
}
else
{
// Get all incoming connections.
var inboundConnections = workflowBlueprint.GetInboundConnectionPath(currentActivityId).ToList();
// Filter out those connections who have a source of IfElse.
var query =
from inboundConnection in inboundConnections
let parentActivityBlueprint = inboundConnection.Source.Activity
where inboundConnection.Source.Activity.Type == nameof(IfElse)
select inboundConnection;
var firstMatch = query.FirstOrDefault();
if (firstMatch != null && firstMatch.Source.Outcome != OutcomeNames.Done)
{
var parentActivityBlueprint = firstMatch.Source.Activity;
var nextActivities = OutcomeResult.GetNextActivities(workflowExecutionContext, parentActivityBlueprint.Id, new[] { OutcomeNames.Done }).ToList();
workflowExecutionContext.ScheduleActivities(nextActivities);
}
}
}
private IEnumerable<string> GetNextActivities(WorkflowExecutionContext workflowExecutionContext, string currentActivityId) => OutcomeResult.GetNextActivities(workflowExecutionContext, currentActivityId, new[] { OutcomeNames.Done });
}
}

View file

@ -0,0 +1,29 @@
using System.Linq;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.ControlFlow
{
public abstract class IteratingActivity : Activity, IBranchingActivity
{
public void Unwind(ActivityExecutionContext context)
{
var workflowExecutionContext = context.WorkflowExecutionContext;
var workflowBlueprint = workflowExecutionContext.WorkflowBlueprint;
var currentActivityId = context.ActivityBlueprint.Id;
var inboundConnections = workflowBlueprint.GetInboundConnectionPath(currentActivityId).ToList();
var query =
from inboundConnection in inboundConnections
let parentActivityBlueprint = inboundConnection.Source.Activity
where inboundConnection.Source.Activity.Type == Type
select inboundConnection;
var firstMatch = query.FirstOrDefault();
if(firstMatch != null && firstMatch.Source.Outcome == OutcomeNames.Iterate)
workflowExecutionContext.ScheduleActivity(firstMatch.Source.Activity.Id);
}
}
}

View file

@ -21,12 +21,26 @@ namespace Elsa.Activities.ControlFlow
[ActivityProperty(Hint = "The conditions to evaluate.")]
public SwitchMode Mode { get; set; } = SwitchMode.MatchFirst;
private bool Evaluated
{
get => GetState<bool>();
set => SetState(value);
}
protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context)
{
if (Evaluated)
{
Evaluated = false;
return Done();
}
var matches = Cases.Where(x => x.Condition).Select(x => x.Name).ToList();
var results = Mode == SwitchMode.MatchFirst ? matches.Any() ? new[] { matches.First() } : new string[0] : matches.ToArray();
var outcomes = new[] { OutcomeNames.Done }.Concat(results);
Evaluated = true;
return Outcomes(outcomes);
}
}

View file

@ -11,7 +11,7 @@ namespace Elsa.Activities.ControlFlow
Description = "Execute while a given condition is true.",
Outcomes = new[] { OutcomeNames.Iterate, OutcomeNames.Done }
)]
public class While : Activity
public class While : IteratingActivity
{
[ActivityProperty(Hint = "The condition to evaluate.")]
public bool Condition { get; set; }

View file

@ -25,7 +25,7 @@ namespace Elsa.ActivityResults
if (previousEntry == null)
return;
var activityId = previousEntry;
var activityId = previousEntry.Source.Activity.Id;
workflowExecutionContext.ScheduleActivity(activityId, _input);
}
}

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Elsa.Services;
using Elsa.Services.Models;
namespace Elsa.Builders
{
@ -108,5 +109,7 @@ namespace Elsa.Builders
PersistWorkflowEnabled = value;
return this;
}
public IWorkflowBlueprint Build() => ((IWorkflowBuilder)WorkflowBuilder).BuildBlueprint();
}
}

View file

@ -3,6 +3,7 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Ccontrolflow_005Cfinish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Ccontrolflow_005Cfor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Ccontrolflow_005Cifthen/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Ccontrolflow_005Cswitch/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Cprimitives_005Csetname/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Csignaling_005Cactivities_005Creceivesignal/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=activities_005Csignaling_005Cactivities_005Csignalreceived/@EntryIndexedValue">True</s:Boolean>

View file

@ -21,6 +21,7 @@ using Elsa.Runtime;
using Elsa.Serialization;
using Elsa.Serialization.Converters;
using Elsa.Services;
using Elsa.Services.Models;
using Elsa.StartupTasks;
using Elsa.Triggers;
using Elsa.WorkflowProviders;
@ -54,6 +55,11 @@ namespace Microsoft.Extensions.DependencyInjection
.AddSingleton(options.StorageFactory)
.AddStartupTask<CreateSubscriptions>();
services
.AddTransient<IBranchingActivity, IfElse>()
.AddTransient<IBranchingActivity, For>()
.AddTransient<IBranchingActivity, While>();
options
.AddWorkflowsCore()
.AddCoreActivities();

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Events;
@ -12,39 +12,32 @@ namespace Elsa.Handlers
/// Walks up the tee of inbound connections along the "Iterate" outcome of the looping construct (While/For/ForEach) and re-schedules the looping activity.
/// Also handles composite activity re-scheduling.
/// </summary>
public class RescheduleLoopsAndContainers : INotificationHandler<WorkflowExecutionBurstCompleted>
public class RescheduleBranchingActivitiesAndContainers : INotificationHandler<WorkflowExecutionBurstCompleted>
{
private readonly IEnumerable<IBranchingActivity> _branchingActivities;
public RescheduleBranchingActivitiesAndContainers(IEnumerable<IBranchingActivity> branchingActivities)
{
_branchingActivities = branchingActivities;
}
public Task Handle(WorkflowExecutionBurstCompleted notification, CancellationToken cancellationToken)
{
ScheduleLoops(notification);
var workflowExecutionContext = notification.WorkflowExecutionContext;
var activityExecutionContext = notification.ActivityExecutionContext;
foreach (var branchingActivity in _branchingActivities)
{
if (workflowExecutionContext.HasScheduledActivities || workflowExecutionContext.Status != WorkflowStatus.Running)
break;
branchingActivity.Unwind(activityExecutionContext);
}
ScheduleContainers(notification);
return Task.CompletedTask;
}
private static void ScheduleLoops(WorkflowExecutionBurstCompleted notification)
{
var workflowExecutionContext = notification.WorkflowExecutionContext;
// If no suspension has been instructed, re-schedule any post-scheduled activities.
if (workflowExecutionContext.HasScheduledActivities || workflowExecutionContext.Status != WorkflowStatus.Running)
return;
var workflowBlueprint = workflowExecutionContext.WorkflowBlueprint;
var currentActivityId = notification.ActivityExecutionContext.ActivityBlueprint.Id;
var inboundConnections = workflowBlueprint.GetInboundConnectionPath(currentActivityId).ToList();
var query =
from inboundConnection in inboundConnections
let parentActivityBlueprint = inboundConnection.Source.Activity
where inboundConnection.Source.Outcome == OutcomeNames.Iterate
select parentActivityBlueprint;
var firstLoop = query.FirstOrDefault();
if(firstLoop != null)
workflowExecutionContext.ScheduleActivity(firstLoop.Id);
}
private static void ScheduleContainers(WorkflowExecutionBurstCompleted notification)
{
var workflowExecutionContext = notification.WorkflowExecutionContext;

View file

@ -18,6 +18,7 @@ namespace Elsa.Services
var activity = _elsaOptions.ActivityFactory.CreateService(type, context.ServiceScope.ServiceProvider);
activity.Data = context.GetData();
activity.Id = context.ActivityId;
await context.WorkflowExecutionContext.WorkflowBlueprint.ActivityPropertyProviders.SetActivityPropertiesAsync(activity, context, context.CancellationToken);
return activity;
}

View file

@ -290,7 +290,6 @@ namespace Elsa.Services
await _mediator.Publish(new ActivityExecuted(activityExecutionContext), cancellationToken);
await result.ExecuteAsync(activityExecutionContext, cancellationToken);
workflowExecutionContext.WorkflowInstance.Output = activityExecutionContext.Output;
workflowExecutionContext.ExecutionLog.Add(activity.Id);
workflowExecutionContext.PruneActivityData();
activityOperation = Execute;
workflowExecutionContext.CompletePass();

View file

@ -146,7 +146,7 @@ namespace Elsa.Triggers
if (workflowInstanceId != null)
{
descriptors = _descriptors![workflowBlueprint.Id].ToList();
descriptors = _descriptors!.ContainsKey(workflowBlueprint.Id) ? _descriptors[workflowBlueprint.Id].ToList() : new List<TriggerDescriptor>();
descriptors.RemoveAll(x => x.WorkflowInstanceId == workflowInstanceId);
var workflowInstance = await _workflowInstanceStore.FindByIdAsync(workflowInstanceId, cancellationToken);

View file

@ -17,23 +17,24 @@ namespace Elsa.Samples.IfElseConsole
public void Build(IWorkflowBuilder workflow)
{
workflow
.WriteLine("--POND OF HAPPINESS--", "Start")
.WriteLine("--POND OF HAPPINESS--")
.WriteLine("Throw some Rupees in and your wishes will surely come true.")
.WriteLine("Do you want to throw Rupees?")
.WriteLine("Do you want to throw Rupees?").WithName("Start")
.ReadLine()
.IfElse(context => IsYes(context.Input),
ifElse =>
{
ifElse
.When(IfElse.True)
.When(OutcomeNames.True)
.WriteLine(GetCurse)
.WriteLine("...")
.Then("Start");
ifElse
.When(IfElse.False)
.WriteLine("Bye.");
});
.When(OutcomeNames.False)
.WriteLine("Keep your rupees.");
})
.WriteLine("--END--");
}
private static bool IsYes(object? value)

View file

@ -17,7 +17,7 @@ namespace Elsa.Samples.SwitchConsole
public void Build(IWorkflowBuilder workflow)
{
workflow
.WriteLine("--Grayscale Calculator--", "Start")
.WriteLine("--Grayscale Calculator--").WithName("Start")
.WriteLine("Enter a number between 0 and 100.")
.ReadLine()
.Switch(cases =>

View file

@ -20,12 +20,12 @@ namespace Elsa.Core.IntegrationTests.Workflows
activity => activity.Set(x => x.Branches, new HashSet<string>(new[] { "Branch 1", "Branch 2", "Branch 3" })),
fork =>
{
fork.When("Branch 1").SignalReceived("Signal1").WriteLine("Branch 1 executed", "WriteLine1").Then("Join");
fork.When("Branch 2").SignalReceived("Signal2").WriteLine("Branch 2 executed", "WriteLine2").Then("Join");
fork.When("Branch 3").SignalReceived("Signal3").WriteLine("Branch 3 executed", "WriteLine3").Then("Join");
fork.When("Branch 1").SignalReceived("Signal1").WriteLine("Branch 1 executed").WithName("WriteLine1").Then("Join");
fork.When("Branch 2").SignalReceived("Signal2").WriteLine("Branch 2 executed").WithName("WriteLine2").Then("Join");
fork.When("Branch 3").SignalReceived("Signal3").WriteLine("Branch 3 executed").WithName("WriteLine3").Then("Join");
})
.Add<Join>(join => join.Set(x => x.Mode, _joinMode)).WithName("Join")
.WriteLine("Finished", "Finished");
.WriteLine("Finished").WithName("Finished");
}
}
}

View file

@ -25,7 +25,7 @@ namespace Elsa.Core.IntegrationTests.Workflows
{
@while
.When(OutcomeNames.Iterate)
.WriteLine(context => $"Inside while loop. Counter = {context.GetVariable<int>(CounterVariableName)}", id: "WriteLoopCount")
.WriteLine(context => $"Inside while loop. Counter = {context.GetVariable<int>(CounterVariableName)}").WithId("WriteLoopCount")
.SetVariable(CounterVariableName, context => GetCounter(context) + 1);
})