< 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
94%
Covered lines: 177
Uncovered lines: 11
Coverable lines: 188
Total lines: 416
Line coverage: 94.1%
Branch coverage
83%
Covered branches: 93
Total branches: 112
Branch coverage: 83%
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;
 7using Elsa.Workflows.Activities.StateMachine.Models;
 8using Elsa.Workflows.Attributes;
 9using Elsa.Workflows.Models;
 10using Elsa.Workflows.Options;
 11using JetBrains.Annotations;
 12
 13namespace Elsa.Workflows.Activities.StateMachine.Activities;
 14
 15/// <summary>
 16/// Executes a state machine made of named states and trigger-driven transitions.
 17/// </summary>
 18[Activity("Elsa", "Flow", "Executes a state machine made of named states and trigger-driven transitions.")]
 19[PublicAPI]
 20public class StateMachine : Activity
 21{
 22    private const string PhaseEntering = "Entering";
 23    private const string PhaseContinuing = "Continuing";
 24    private const string CurrentStateProperty = "CurrentState";
 3425    private readonly Inline _automaticTransitionContinuation = new();
 26
 27    /// <inheritdoc />
 3428    public StateMachine([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line
 29    {
 3430    }
 31
 32    /// <summary>
 33    /// The states in declaration order.
 34    /// </summary>
 31135    public ICollection<StateMachineState> States { get; set; } = new List<StateMachineState>();
 36
 37    /// <summary>
 38    /// The transitions in declaration order.
 39    /// </summary>
 37940    public ICollection<Transition> Transitions { get; set; } = new List<Transition>();
 41
 42    /// <summary>
 43    /// The first state to enter when no current state is set.
 44    /// </summary>
 5845    public string? InitialState { get; set; }
 46
 47    /// <summary>
 48    /// The currently active state.
 49    /// </summary>
 7150    public string? CurrentState { get; set; }
 51
 52    /// <summary>
 53    /// Exposes nested activities to the workflow graph builder.
 54    /// </summary>
 55    [JsonIgnore]
 56    [Browsable(false)]
 2657    public IEnumerable<IActivity> Activities => GetActivities();
 58
 59    /// <inheritdoc />
 60    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
 61    {
 2662        EnsureSupportedTriggerIdentities();
 63
 2464        var currentState = GetCurrentState(context);
 2465        SetCurrentState(context, string.IsNullOrWhiteSpace(currentState) ? InitialState : currentState);
 66
 2467        if (FindState(GetCurrentState(context)) == null)
 68        {
 069            await context.CompleteActivityAsync();
 070            return;
 71        }
 72
 2473        await EnterStateAsync(context);
 2474    }
 75
 76    private async ValueTask EnterStateAsync(ActivityExecutionContext context, ActivityExecutionContext? schedulingContex
 77    {
 3778        var state = FindState(GetCurrentState(context));
 79
 3780        if (state == null)
 81        {
 082            await context.CompleteActivityAsync();
 083            return;
 84        }
 85
 3786        if (state.Entry != null)
 87        {
 2088            await ScheduleAsync(context, state.Entry, OnStateEntryCompletedAsync, PhaseEntering, schedulingContext);
 2089            return;
 90        }
 91
 1792        await ScheduleOutboundTriggersAsync(context, schedulingContext);
 3793    }
 94
 95    private async ValueTask OnStateEntryCompletedAsync(ActivityCompletedContext context)
 96    {
 1797        await ScheduleOutboundTriggersAsync(context.TargetContext, context.ChildContext);
 1798    }
 99
 100    private async ValueTask ScheduleOutboundTriggersAsync(ActivityExecutionContext context, ActivityExecutionContext? sc
 101    {
 82102        var outboundTransitions = GetOutboundTransitions(GetCurrentState(context)).Where(x => FindState(x.To) != null).T
 103
 34104        if (!outboundTransitions.Any())
 105        {
 4106            await context.CompleteActivityAsync();
 4107            return;
 108        }
 109
 119110        foreach (var transition in outboundTransitions.Where(x => x.Trigger == null))
 111        {
 11112            if (await TryTakeTransitionAsync(context, transition, schedulingContext))
 9113                return;
 114        }
 115
 58116        var triggeredTransitions = outboundTransitions.Where(x => x.Trigger != null).ToList();
 117
 21118        if (!triggeredTransitions.Any())
 1119            return;
 120
 110121        foreach (var transition in triggeredTransitions)
 35122            await ScheduleAsync(context, transition.Trigger!, OnTriggerCompletedAsync, GetTransitionKey(transition), sch
 34123    }
 124
 125    private async ValueTask OnTriggerCompletedAsync(ActivityCompletedContext context)
 126    {
 15127        var targetContext = context.TargetContext;
 15128        var transition = FindTransitionByKey(targetContext, GetCompletionTag(context)) ?? FindTransitionByTrigger(target
 129
 15130        if (transition == null || !IsCurrentSource(targetContext, transition) || FindState(transition.To) == null)
 0131            return;
 132
 15133        if (!await TryTakeTransitionAsync(targetContext, transition, context.ChildContext))
 134        {
 2135            if (transition.Trigger != null)
 2136                await ScheduleAsync(targetContext, transition.Trigger, OnTriggerCompletedAsync, GetTransitionKey(transit
 137        }
 15138    }
 139
 140    private async ValueTask<bool> TryTakeTransitionAsync(
 141        ActivityExecutionContext context,
 142        Transition transition,
 143        ActivityExecutionContext? schedulingContext)
 144    {
 26145        var canTransition = transition.Condition == null || await EvaluateConditionAsync(context, transition.Condition);
 146
 26147        if (!canTransition)
 4148            return false;
 149
 22150        await CancelCompetingTriggersAsync(context, transition, schedulingContext);
 22151        await ExitStateAsync(context, transition, schedulingContext);
 22152        return true;
 26153    }
 154
 155    private async ValueTask RunTransitionActionAsync(ActivityExecutionContext context, Transition transition, ActivityEx
 156    {
 18157        if (!IsCurrentSource(context, transition))
 0158            return;
 159
 18160        if (transition.Action != null)
 161        {
 7162            await ScheduleTransitionActivityAsync(context, transition.Action, transition, OnTransitionActionCompletedAsy
 7163            return;
 164        }
 165
 11166        await CompleteTransitionAsync(context, transition, schedulingContext);
 18167    }
 168
 169    private async ValueTask OnTransitionActionCompletedAsync(ActivityCompletedContext context)
 170    {
 6171        var targetContext = context.TargetContext;
 6172        var transition = FindTransitionByKey(targetContext, GetCompletionTag(context));
 173
 6174        if (transition == null || !IsCurrentSource(targetContext, transition))
 1175            return;
 176
 5177        await CompleteTransitionAsync(targetContext, transition, context.ChildContext);
 6178    }
 179
 180    private async ValueTask ExitStateAsync(ActivityExecutionContext context, Transition transition, ActivityExecutionCon
 181    {
 22182        var sourceState = FindState(transition.From);
 183
 22184        if (sourceState?.Exit != null)
 185        {
 10186            await ScheduleTransitionActivityAsync(context, sourceState.Exit, transition, OnStateExitCompletedAsync, sche
 10187            return;
 188        }
 189
 12190        await RunTransitionActionAsync(context, transition, schedulingContext);
 22191    }
 192
 193    private async ValueTask OnStateExitCompletedAsync(ActivityCompletedContext context)
 194    {
 7195        var targetContext = context.TargetContext;
 7196        var transition = FindTransitionByKey(targetContext, GetCompletionTag(context));
 197
 7198        if (transition == null || !IsCurrentSource(targetContext, transition))
 1199            return;
 200
 6201        await RunTransitionActionAsync(targetContext, transition, context.ChildContext);
 7202    }
 203
 204    private async ValueTask CompleteTransitionAsync(ActivityExecutionContext context, Transition transition, ActivityExe
 205    {
 16206        SetCurrentState(context, transition.To);
 207
 208        // Automatic transitions are normally evaluated inline after entering a state. If the
 209        // target is part of a triggerless cycle, queue the next evaluation instead. This keeps
 210        // the WF4 ordering (the transition has completed and the target state is current) while
 211        // allowing the workflow scheduler to unwind the current call stack between iterations.
 16212        if (HasAutomaticCycle(GetCurrentState(context)))
 213        {
 6214            await ScheduleAsync(context, _automaticTransitionContinuation, OnAutomaticTransitionContinuationCompletedAsy
 6215            return;
 216        }
 217
 10218        await EnterStateAsync(context, schedulingContext);
 16219    }
 220
 221    private async ValueTask OnAutomaticTransitionContinuationCompletedAsync(ActivityCompletedContext context)
 222    {
 3223        await EnterStateAsync(context.TargetContext, context.ChildContext);
 3224    }
 225
 226    private async ValueTask ScheduleTransitionActivityAsync(
 227        ActivityExecutionContext context,
 228        IActivity activity,
 229        Transition transition,
 230        ActivityCompletionCallback callback,
 231        ActivityExecutionContext? schedulingContext)
 232    {
 17233        await ScheduleAsync(context, activity, callback, GetTransitionKey(transition), schedulingContext);
 17234    }
 235
 236    private static async ValueTask ScheduleAsync(
 237        ActivityExecutionContext context,
 238        IActivity activity,
 239        ActivityCompletionCallback callback,
 240        string tag,
 241        ActivityExecutionContext? schedulingContext)
 242    {
 80243        var options = new ScheduleWorkOptions
 80244        {
 80245            CompletionCallback = callback,
 80246            Tag = tag,
 80247            SchedulingActivityExecutionId = schedulingContext?.Id
 80248        };
 249
 80250        await context.ScheduleActivityAsync(activity, options);
 80251    }
 252
 253    private async Task CancelCompetingTriggersAsync(ActivityExecutionContext context, Transition winningTransition, Acti
 254    {
 22255        var competingTriggerIds = GetOutboundTransitions(winningTransition.From)
 37256            .Where(x => !ReferenceEquals(x, winningTransition))
 15257            .Select(x => x.Trigger?.Id)
 15258            .Where(x => !string.IsNullOrWhiteSpace(x))
 14259            .Select(x => x!)
 22260            .ToHashSet();
 261
 22262        var competingTriggerContexts = context.WorkflowExecutionContext.ActivityExecutionContexts
 35263            .Where(x => x.ParentActivityExecutionContext == context && !ReferenceEquals(x, winningTriggerContext) && com
 22264            .ToList();
 265
 52266        foreach (var competingTriggerContext in competingTriggerContexts)
 4267            await competingTriggerContext.CancelActivityAsync();
 268
 22269        RemoveScheduledCompetingTriggers(context, competingTriggerIds);
 22270        RemoveCompetingTriggerCallbacks(context, competingTriggerIds);
 22271    }
 272
 273    private static void RemoveScheduledCompetingTriggers(ActivityExecutionContext context, HashSet<string> competingTrig
 274    {
 22275        var scheduler = context.WorkflowExecutionContext.Scheduler;
 61276        scheduler.RemoveWhere(x => IsCompetingTriggerWorkItem(context, competingTriggerIds, x));
 22277    }
 278
 279    private static bool IsCompetingTriggerWorkItem(ActivityExecutionContext context, HashSet<string> competingTriggerIds
 39280        workItem.Owner == context && competingTriggerIds.Contains(workItem.Activity.Id);
 281
 282    private static void RemoveCompetingTriggerCallbacks(ActivityExecutionContext context, HashSet<string> competingTrigg
 283    {
 22284        var competingTriggerCallbacks = context.WorkflowExecutionContext.CompletionCallbacks
 13285            .Where(x => x.Owner == context && competingTriggerIds.Contains(x.Child.Activity.Id))
 22286            .ToList();
 287
 22288        context.WorkflowExecutionContext.RemoveCompletionCallbacks(competingTriggerCallbacks);
 22289    }
 290
 209291    private string? GetCurrentState(ActivityExecutionContext context) => context.GetProperty<string>(CurrentStatePropert
 292
 293    private IEnumerable<IActivity> GetActivities()
 294    {
 361295        foreach (var stateActivity in States.SelectMany(x => new[] { x.Entry, x.Exit }).Where(x => x != null))
 51296            yield return stateActivity!;
 297
 26298        var seenTriggerInstances = new HashSet<IActivity>(ReferenceEqualityComparer.Instance);
 26299        var seenTriggerIds = new HashSet<string>(StringComparer.Ordinal);
 300
 184301        foreach (var transition in Transitions)
 302        {
 66303            if (transition.Trigger != null && seenTriggerInstances.Add(transition.Trigger) &&
 66304                (string.IsNullOrWhiteSpace(transition.Trigger.Id) || seenTriggerIds.Add(transition.Trigger.Id)))
 54305                yield return transition.Trigger;
 306
 66307            if (transition.Action != null)
 19308                yield return transition.Action;
 66309        }
 310
 26311        yield return _automaticTransitionContinuation;
 26312    }
 313
 314    private void EnsureSupportedTriggerIdentities()
 315    {
 148316        var triggers = Transitions.Where(x => x.Trigger != null).Select(x => x.Trigger!).ToList();
 26317        var seenInstances = new HashSet<IActivity>(ReferenceEqualityComparer.Instance);
 26318        var seenIds = new HashSet<string>(StringComparer.Ordinal);
 319
 158320        foreach (var trigger in triggers)
 321        {
 54322            var sharesInstance = !seenInstances.Add(trigger);
 54323            var duplicatesId = !string.IsNullOrWhiteSpace(trigger.Id) && !seenIds.Add(trigger.Id);
 324
 54325            if (sharesInstance || duplicatesId)
 2326                throw new InvalidOperationException("StateMachine transitions cannot share a Trigger activity in Elsa 3.
 327        }
 24328    }
 329
 330    private void SetCurrentState(ActivityExecutionContext context, string? state)
 331    {
 40332        CurrentState = state;
 333
 40334        if (state == null)
 0335            context.RemoveProperty(CurrentStateProperty);
 336        else
 40337            context.SetProperty(CurrentStateProperty, state);
 40338    }
 339
 46340    private bool IsCurrentSource(ActivityExecutionContext context, Transition transition) => string.Equals(transition.Fr
 341
 342    private static async Task<bool> EvaluateConditionAsync(ActivityExecutionContext context, Input<bool> condition)
 343    {
 15344        var evaluator = context.GetRequiredService<IExpressionEvaluator>();
 15345        return await evaluator.EvaluateAsync(condition, context.ExpressionExecutionContext);
 15346    }
 347
 348    private StateMachineState? FindState(string? name) =>
 154349        string.IsNullOrWhiteSpace(name)
 154350            ? null
 403351            : States.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.Ordinal));
 352
 353    private IEnumerable<Transition> GetOutboundTransitions(string? sourceState) =>
 102354        string.IsNullOrWhiteSpace(sourceState)
 102355            ? []
 312356            : Transitions.Where(x => string.Equals(x.From, sourceState, StringComparison.Ordinal));
 357
 358    private bool HasAutomaticCycle(string? startingState)
 359    {
 16360        if (string.IsNullOrWhiteSpace(startingState))
 0361            return false;
 362
 16363        var visitedStates = new HashSet<string>(StringComparer.Ordinal);
 16364        var statesToVisit = new Stack<string>([startingState]);
 365
 28366        while (statesToVisit.TryPop(out var state))
 367        {
 62368            foreach (var transition in GetOutboundTransitions(state).Where(x => x.Trigger == null && FindState(x.To) != 
 369            {
 8370                if (string.Equals(transition.To, startingState, StringComparison.Ordinal))
 6371                    return true;
 372
 2373                if (visitedStates.Add(transition.To!))
 2374                    statesToVisit.Push(transition.To!);
 375            }
 376        }
 377
 10378        return false;
 6379    }
 380
 381    private Transition? FindTransitionByTrigger(ActivityExecutionContext context, IActivity trigger) =>
 0382        GetOutboundTransitions(GetCurrentState(context)).FirstOrDefault(x => ReferenceEquals(x.Trigger, trigger));
 383
 384    private Transition? FindTransitionByKey(ActivityExecutionContext context, string? key)
 385    {
 28386        if (string.IsNullOrWhiteSpace(key))
 0387            return null;
 388
 28389        var currentState = GetCurrentState(context);
 59390        return GetOutboundTransitions(currentState).FirstOrDefault(x => string.Equals(GetTransitionKey(x), key, StringCo
 30391               ?? Transitions.FirstOrDefault(x => string.Equals(GetTransitionKey(x), key, StringComparison.Ordinal));
 392    }
 393
 394    private string GetTransitionKey(Transition transition)
 395    {
 87396        var index = 0;
 339397        foreach (var current in Transitions)
 398        {
 126399            if (ReferenceEquals(current, transition))
 87400                return $"{index}:{GetTransitionDisplayKey(transition)}";
 401
 39402            index++;
 403        }
 404
 0405        return GetTransitionDisplayKey(transition);
 87406    }
 407
 408    private static string GetTransitionDisplayKey(Transition transition) =>
 87409        string.IsNullOrWhiteSpace(transition.Name)
 87410            ? $"{transition.From}->{transition.To}"
 87411            : transition.Name;
 412
 413    private static string? GetCompletionTag(ActivityCompletedContext context) =>
 28414        context.TargetContext.Tag as string ?? context.ChildContext.Tag as string;
 415
 416}

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()
TryTakeTransitionAsync()
RunTransitionActionAsync()
OnTransitionActionCompletedAsync()
ExitStateAsync()
OnStateExitCompletedAsync()
CompleteTransitionAsync()
OnAutomaticTransitionContinuationCompletedAsync()
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)
GetActivities()
EnsureSupportedTriggerIdentities()
SetCurrentState(Elsa.Workflows.ActivityExecutionContext,System.String)
IsCurrentSource(Elsa.Workflows.ActivityExecutionContext,Elsa.Workflows.Activities.StateMachine.Models.Transition)
EvaluateConditionAsync()
FindState(System.String)
GetOutboundTransitions(System.String)
HasAutomaticCycle(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)
GetCompletionTag(Elsa.Workflows.ActivityCompletedContext)