< Summary

Information
Class: Elsa.Workflows.Activities.StateMachine.Activities.StateMachine
Assembly: Elsa.Workflows.Core
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Activities/StateMachine/Activities/StateMachine.cs
Line coverage
92%
Covered lines: 131
Uncovered lines: 10
Coverable lines: 141
Total lines: 317
Line coverage: 92.9%
Branch coverage
76%
Covered branches: 61
Total branches: 80
Branch coverage: 76.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Core/Activities/StateMachine/Activities/StateMachine.cs

#LineLine coverage
 1using System.ComponentModel;
 2using System.Runtime.CompilerServices;
 3using System.Text.Json.Serialization;
 4using Elsa.Expressions.Contracts;
 5using Elsa.Extensions;
 6using Elsa.Workflows.Activities.StateMachine.Models;
 7using Elsa.Workflows.Attributes;
 8using Elsa.Workflows.Models;
 9using Elsa.Workflows.Options;
 10using JetBrains.Annotations;
 11
 12namespace Elsa.Workflows.Activities.StateMachine.Activities;
 13
 14/// <summary>
 15/// Executes a state machine made of named states and trigger-driven transitions.
 16/// </summary>
 17[Activity("Elsa", "Flow", "Executes a state machine made of named states and trigger-driven transitions.")]
 18[PublicAPI]
 19public class StateMachine : Activity
 20{
 21    private const string PhaseEntering = "Entering";
 22    private const string CurrentStateProperty = "CurrentState";
 23
 24    /// <inheritdoc />
 1325    public StateMachine([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line
 26    {
 1327    }
 28
 29    /// <summary>
 30    /// The states in declaration order.
 31    /// </summary>
 13432    public ICollection<StateMachineState> States { get; set; } = new List<StateMachineState>();
 33
 34    /// <summary>
 35    /// The transitions in declaration order.
 36    /// </summary>
 15537    public ICollection<Transition> Transitions { get; set; } = new List<Transition>();
 38
 39    /// <summary>
 40    /// The first state to enter when no current state is set.
 41    /// </summary>
 2442    public string? InitialState { get; set; }
 43
 44    /// <summary>
 45    /// The currently active state.
 46    /// </summary>
 3147    public string? CurrentState { get; set; }
 48
 49    /// <summary>
 50    /// Exposes nested activities to the workflow graph builder.
 51    /// </summary>
 52    [JsonIgnore]
 53    [Browsable(false)]
 54    public IEnumerable<IActivity> Activities =>
 4355        States.SelectMany(x => new[] { x.Entry, x.Exit })
 3256            .Concat(Transitions.SelectMany(x => new[] { x.Trigger, x.Action }))
 12857            .Where(x => x != null)
 1158            .Cast<IActivity>();
 59
 60    /// <inheritdoc />
 61    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
 62    {
 1163        var currentState = GetCurrentState(context);
 1164        SetCurrentState(context, string.IsNullOrWhiteSpace(currentState) ? InitialState : currentState);
 65
 1166        if (FindState(GetCurrentState(context)) == null)
 67        {
 068            await context.CompleteActivityAsync();
 069            return;
 70        }
 71
 1172        await EnterStateAsync(context);
 1173    }
 74
 75    private async ValueTask EnterStateAsync(ActivityExecutionContext context, ActivityExecutionContext? schedulingContex
 76    {
 1677        var state = FindState(GetCurrentState(context));
 78
 1679        if (state == null)
 80        {
 081            await context.CompleteActivityAsync();
 082            return;
 83        }
 84
 1685        if (state.Entry != null)
 86        {
 1087            await ScheduleAsync(context, state.Entry, OnStateEntryCompletedAsync, PhaseEntering, schedulingContext);
 1088            return;
 89        }
 90
 691        await ScheduleOutboundTriggersAsync(context, schedulingContext);
 1692    }
 93
 94    private async ValueTask OnStateEntryCompletedAsync(ActivityCompletedContext context)
 95    {
 996        await ScheduleOutboundTriggersAsync(context.TargetContext, context.ChildContext);
 997    }
 98
 99    private async ValueTask ScheduleOutboundTriggersAsync(ActivityExecutionContext context, ActivityExecutionContext? sc
 100    {
 39101        var outboundTransitions = GetOutboundTransitions(GetCurrentState(context)).Where(x => x.Trigger != null && FindS
 102
 15103        if (!outboundTransitions.Any())
 104        {
 1105            await context.CompleteActivityAsync();
 1106            return;
 107        }
 108
 76109        foreach (var transition in outboundTransitions)
 24110            await ScheduleAsync(context, transition.Trigger!, OnTriggerCompletedAsync, GetTransitionKey(transition), sch
 15111    }
 112
 113    private async ValueTask OnTriggerCompletedAsync(ActivityCompletedContext context)
 114    {
 11115        var targetContext = context.TargetContext;
 11116        var transition = FindTransitionByKey(targetContext, context.ChildContext.Tag as string) ?? FindTransitionByTrigg
 117
 11118        if (transition == null || !IsCurrentSource(targetContext, transition) || FindState(transition.To) == null)
 0119            return;
 120
 11121        var canTransition = transition.Condition == null || await EvaluateConditionAsync(targetContext, transition.Condi
 122
 11123        if (!canTransition)
 124        {
 2125            if (transition.Trigger != null)
 2126                await ScheduleAsync(targetContext, transition.Trigger, OnTriggerCompletedAsync, GetTransitionKey(transit
 127
 2128            return;
 129        }
 130
 9131        await CancelCompetingTriggersAsync(targetContext, transition, context.ChildContext);
 132
 9133        if (transition.Action != null)
 134        {
 4135            await ScheduleTransitionActivityAsync(targetContext, transition.Action, transition, OnTransitionActionComple
 4136            return;
 137        }
 138
 5139        await ExitStateAsync(targetContext, transition, context.ChildContext);
 11140    }
 141
 142    private async ValueTask OnTransitionActionCompletedAsync(ActivityCompletedContext context)
 143    {
 2144        var targetContext = context.TargetContext;
 2145        var transition = FindTransitionByKey(targetContext, context.ChildContext.Tag as string);
 146
 2147        if (transition == null || !IsCurrentSource(targetContext, transition))
 1148            return;
 149
 1150        await ExitStateAsync(targetContext, transition, context.ChildContext);
 2151    }
 152
 153    private async ValueTask ExitStateAsync(ActivityExecutionContext context, Transition transition, ActivityExecutionCon
 154    {
 6155        var sourceState = FindState(transition.From);
 156
 6157        if (sourceState?.Exit != null)
 158        {
 2159            await ScheduleTransitionActivityAsync(context, sourceState.Exit, transition, OnStateExitCompletedAsync, sche
 2160            return;
 161        }
 162
 4163        await CompleteTransitionAsync(context, transition, schedulingContext);
 6164    }
 165
 166    private async ValueTask OnStateExitCompletedAsync(ActivityCompletedContext context)
 167    {
 1168        var targetContext = context.TargetContext;
 1169        var transition = FindTransitionByKey(targetContext, context.ChildContext.Tag as string);
 170
 1171        if (transition == null || !IsCurrentSource(targetContext, transition))
 0172            return;
 173
 1174        await CompleteTransitionAsync(targetContext, transition, context.ChildContext);
 1175    }
 176
 177    private async ValueTask CompleteTransitionAsync(ActivityExecutionContext context, Transition transition, ActivityExe
 178    {
 5179        SetCurrentState(context, transition.To);
 5180        await EnterStateAsync(context, schedulingContext);
 5181    }
 182
 183    private async ValueTask ScheduleTransitionActivityAsync(
 184        ActivityExecutionContext context,
 185        IActivity activity,
 186        Transition transition,
 187        ActivityCompletionCallback callback,
 188        ActivityExecutionContext? schedulingContext)
 189    {
 6190        await ScheduleAsync(context, activity, callback, GetTransitionKey(transition), schedulingContext);
 6191    }
 192
 193    private static async ValueTask ScheduleAsync(
 194        ActivityExecutionContext context,
 195        IActivity activity,
 196        ActivityCompletionCallback callback,
 197        string tag,
 198        ActivityExecutionContext? schedulingContext)
 199    {
 42200        var options = new ScheduleWorkOptions
 42201        {
 42202            CompletionCallback = callback,
 42203            Tag = tag,
 42204            SchedulingActivityExecutionId = schedulingContext?.Id
 42205        };
 206
 42207        await context.ScheduleActivityAsync(activity, options);
 42208    }
 209
 210    private async Task CancelCompetingTriggersAsync(ActivityExecutionContext context, Transition winningTransition, Acti
 211    {
 9212        var competingTriggerIds = GetOutboundTransitions(winningTransition.From)
 17213            .Where(x => !ReferenceEquals(x, winningTransition))
 8214            .Select(x => x.Trigger?.Id)
 8215            .Where(x => !string.IsNullOrWhiteSpace(x))
 8216            .Select(x => x!)
 9217            .ToHashSet();
 218
 9219        var competingTriggerContexts = context.WorkflowExecutionContext.ActivityExecutionContexts
 18220            .Where(x => x.ParentActivityExecutionContext == context && !ReferenceEquals(x, winningTriggerContext) && com
 9221            .ToList();
 222
 22223        foreach (var competingTriggerContext in competingTriggerContexts)
 2224            await competingTriggerContext.CancelActivityAsync();
 225
 9226        RemoveScheduledCompetingTriggers(context, competingTriggerIds);
 9227        RemoveCompetingTriggerCallbacks(context, competingTriggerIds);
 9228    }
 229
 230    private static void RemoveScheduledCompetingTriggers(ActivityExecutionContext context, HashSet<string> competingTrig
 231    {
 9232        var scheduler = context.WorkflowExecutionContext.Scheduler;
 9233        var scheduledWorkItems = scheduler.List().ToList();
 234
 30235        if (!scheduledWorkItems.Any(x => IsCompetingTriggerWorkItem(context, competingTriggerIds, x)))
 1236            return;
 237
 8238        scheduler.Clear();
 239
 67240        foreach (var workItem in scheduledWorkItems.Where(x => !IsCompetingTriggerWorkItem(context, competingTriggerIds,
 14241            scheduler.Schedule(workItem);
 8242    }
 243
 244    private static bool IsCompetingTriggerWorkItem(ActivityExecutionContext context, HashSet<string> competingTriggerIds
 44245        workItem.Owner == context && competingTriggerIds.Contains(workItem.Activity.Id);
 246
 247    private static void RemoveCompetingTriggerCallbacks(ActivityExecutionContext context, HashSet<string> competingTrigg
 248    {
 9249        var competingTriggerCallbacks = context.WorkflowExecutionContext.CompletionCallbacks
 8250            .Where(x => x.Owner == context && competingTriggerIds.Contains(x.Child.Activity.Id))
 9251            .ToList();
 252
 9253        context.WorkflowExecutionContext.RemoveCompletionCallbacks(competingTriggerCallbacks);
 9254    }
 255
 81256    private string? GetCurrentState(ActivityExecutionContext context) => context.GetProperty<string>(CurrentStatePropert
 257
 258    private void SetCurrentState(ActivityExecutionContext context, string? state)
 259    {
 16260        CurrentState = state;
 261
 16262        if (state == null)
 0263            context.RemoveProperty(CurrentStateProperty);
 264        else
 16265            context.SetProperty(CurrentStateProperty, state);
 16266    }
 267
 14268    private bool IsCurrentSource(ActivityExecutionContext context, Transition transition) => string.Equals(transition.Fr
 269
 270    private static async Task<bool> EvaluateConditionAsync(ActivityExecutionContext context, Input<bool> condition)
 271    {
 7272        var evaluator = context.GetRequiredService<IExpressionEvaluator>();
 7273        return await evaluator.EvaluateAsync(condition, context.ExpressionExecutionContext);
 7274    }
 275
 276    private StateMachineState? FindState(string? name) =>
 68277        string.IsNullOrWhiteSpace(name)
 68278            ? null
 193279            : States.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.Ordinal));
 280
 281    private IEnumerable<Transition> GetOutboundTransitions(string? sourceState) =>
 38282        string.IsNullOrWhiteSpace(sourceState)
 38283            ? []
 129284            : Transitions.Where(x => string.Equals(x.From, sourceState, StringComparison.Ordinal));
 285
 286    private Transition? FindTransitionByTrigger(ActivityExecutionContext context, IActivity trigger) =>
 0287        GetOutboundTransitions(GetCurrentState(context)).FirstOrDefault(x => ReferenceEquals(x.Trigger, trigger));
 288
 289    private Transition? FindTransitionByKey(ActivityExecutionContext context, string? key)
 290    {
 14291        if (string.IsNullOrWhiteSpace(key))
 0292            return null;
 293
 14294        var currentState = GetCurrentState(context);
 31295        return GetOutboundTransitions(currentState).FirstOrDefault(x => string.Equals(GetTransitionKey(x), key, StringCo
 15296               ?? Transitions.FirstOrDefault(x => string.Equals(GetTransitionKey(x), key, StringComparison.Ordinal));
 297    }
 298
 299    private string GetTransitionKey(Transition transition)
 300    {
 50301        var index = 0;
 204302        foreach (var current in Transitions)
 303        {
 77304            if (ReferenceEquals(current, transition))
 50305                return $"{index}:{GetTransitionDisplayKey(transition)}";
 306
 27307            index++;
 308        }
 309
 0310        return GetTransitionDisplayKey(transition);
 50311    }
 312
 313    private static string GetTransitionDisplayKey(Transition transition) =>
 50314        string.IsNullOrWhiteSpace(transition.Name)
 50315            ? $"{transition.From}->{transition.To}"
 50316            : transition.Name;
 317}

Methods/Properties

.ctor(System.String,System.Nullable`1<System.Int32>)
get_States()
get_Transitions()
get_InitialState()
get_CurrentState()
get_Activities()
ExecuteAsync()
EnterStateAsync()
OnStateEntryCompletedAsync()
ScheduleOutboundTriggersAsync()
OnTriggerCompletedAsync()
OnTransitionActionCompletedAsync()
ExitStateAsync()
OnStateExitCompletedAsync()
CompleteTransitionAsync()
ScheduleTransitionActivityAsync()
ScheduleAsync()
CancelCompetingTriggersAsync()
RemoveScheduledCompetingTriggers(Elsa.Workflows.ActivityExecutionContext,System.Collections.Generic.HashSet`1<System.String>)
IsCompetingTriggerWorkItem(Elsa.Workflows.ActivityExecutionContext,System.Collections.Generic.HashSet`1<System.String>,Elsa.Workflows.Models.ActivityWorkItem)
RemoveCompetingTriggerCallbacks(Elsa.Workflows.ActivityExecutionContext,System.Collections.Generic.HashSet`1<System.String>)
GetCurrentState(Elsa.Workflows.ActivityExecutionContext)
SetCurrentState(Elsa.Workflows.ActivityExecutionContext,System.String)
IsCurrentSource(Elsa.Workflows.ActivityExecutionContext,Elsa.Workflows.Activities.StateMachine.Models.Transition)
EvaluateConditionAsync()
FindState(System.String)
GetOutboundTransitions(System.String)
FindTransitionByTrigger(Elsa.Workflows.ActivityExecutionContext,Elsa.Workflows.IActivity)
FindTransitionByKey(Elsa.Workflows.ActivityExecutionContext,System.String)
GetTransitionKey(Elsa.Workflows.Activities.StateMachine.Models.Transition)
GetTransitionDisplayKey(Elsa.Workflows.Activities.StateMachine.Models.Transition)