using Elsa.Testing.Shared; using Elsa.Mediator.Contracts; using Elsa.Workflows; using Elsa.Workflows.Activities.StateMachine.Models; using Elsa.Workflows.Options; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using NSubstitute; using StateMachineActivity = Elsa.Workflows.Activities.StateMachine.Activities.StateMachine; namespace Elsa.Activities.UnitTests.StateMachine; public class StateMachineTests { private const string CurrentStateProperty = "CurrentState"; private readonly WriteLine _newEntry = new("enter new") { Id = "new-entry" }; private readonly WriteLine _newExit = new("exit new") { Id = "new-exit" }; private readonly WriteLine _paidEntry = new("enter paid") { Id = "paid-entry" }; private readonly WriteLine _payTrigger = new("pay trigger") { Id = "pay-trigger" }; private readonly WriteLine _cancelTrigger = new("cancel trigger") { Id = "cancel-trigger" }; private readonly WriteLine _paidTrigger = new("paid trigger") { Id = "paid-trigger" }; private readonly WriteLine _payAction = new("pay action") { Id = "pay-action" }; private readonly StateMachineActivity _stateMachine; public StateMachineTests() { _stateMachine = new StateMachineActivity { InitialState = "New", States = { new StateMachineState { Name = "New", Entry = _newEntry, Exit = _newExit }, new StateMachineState { Name = "Paid", Entry = _paidEntry }, new StateMachineState { Name = "Closed" } }, Transitions = { new Transition { Name = "Pay", From = "New", To = "Paid", Trigger = _payTrigger, Condition = new(true), Action = _payAction }, new Transition { Name = "Cancel", From = "New", To = "Closed", Trigger = _cancelTrigger, Condition = new(true) }, new Transition { Name = "Close", From = "Paid", To = "Closed", Trigger = _paidTrigger } } }; } [Fact(DisplayName = "StateMachine schedules initial state entry before outbound triggers")] public async Task SchedulesInitialStateEntryBeforeOutboundTriggers() { var context = await ExecuteAsync(); Assert.Equal("New", _stateMachine.CurrentState); Assert.True(context.HasScheduledActivity(_newEntry)); Assert.False(context.HasScheduledActivity(_payTrigger)); } [Fact(DisplayName = "StateMachine schedules outbound triggers after entry completes")] public async Task SchedulesOutboundTriggersAfterEntryCompletes() { var context = await ExecuteAsync(); await CompleteScheduledActivityAsync(context, _newEntry); Assert.True(context.HasScheduledActivity(_payTrigger)); Assert.True(context.HasScheduledActivity(_cancelTrigger)); Assert.Equal(ActivityStatus.Running, context.Status); } [Fact(DisplayName = "StateMachine executes source exit, accepted transition action, target entry and target triggers")] public async Task ExecutesAcceptedTransitionPath() { var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.True(context.HasScheduledActivity(_newExit)); Assert.False(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _newExit); Assert.True(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _payAction); Assert.Equal("Paid", _stateMachine.CurrentState); Assert.Equal("Paid", context.GetProperty(CurrentStateProperty)); Assert.True(context.HasScheduledActivity(_paidEntry)); await CompleteScheduledActivityAsync(context, _paidEntry); Assert.True(context.HasScheduledActivity(_paidTrigger)); Assert.Equal(ActivityStatus.Running, context.Status); } [Fact(DisplayName = "StateMachine exposes source entry, trigger, exit, action and target entry order")] public async Task AcceptedTransitionExecutesObservableLifecycleOrder() { var lifecycle = new List(); var context = await ExecuteAsync(); Assert.True(context.HasScheduledActivity(_newEntry)); Assert.False(context.HasScheduledActivity(_payTrigger)); await CompleteScheduledActivityAsync(context, _newEntry); lifecycle.Add("source entry"); Assert.True(context.HasScheduledActivity(_payTrigger)); await CompleteScheduledActivityAsync(context, _payTrigger); lifecycle.Add("trigger"); Assert.True(context.HasScheduledActivity(_newExit)); Assert.False(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _newExit); lifecycle.Add("source exit"); Assert.True(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _payAction); lifecycle.Add("action"); Assert.Equal("Paid", context.GetProperty(CurrentStateProperty)); Assert.True(context.HasScheduledActivity(_paidEntry)); await CompleteScheduledActivityAsync(context, _paidEntry); lifecycle.Add("target entry"); Assert.True(context.HasScheduledActivity(_paidTrigger)); Assert.Equal(new[] { "source entry", "trigger", "source exit", "action", "target entry" }, lifecycle); } [Fact(DisplayName = "StateMachine treats missing transition condition as true")] public async Task MissingConditionAllowsTransition() { var payTransition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); payTransition.Condition = null; payTransition.Action = null; _stateMachine.States.Single(x => x.Name == "New").Exit = null; _stateMachine.States.Single(x => x.Name == "Paid").Entry = null; var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.Equal("Paid", _stateMachine.CurrentState); Assert.True(context.HasScheduledActivity(_paidTrigger)); } [Fact(DisplayName = "StateMachine evaluates a triggerless transition immediately after state entry")] public async Task TriggerlessTransitionIsEvaluatedImmediately() { var payTransition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); payTransition.Trigger = null; payTransition.Condition = null; var context = await ExecuteAsync(); await CompleteScheduledActivityAsync(context, _newEntry); Assert.True(context.HasScheduledActivity(_newExit)); Assert.False(context.HasScheduledActivity(_cancelTrigger)); await CompleteScheduledActivityAsync(context, _newExit); await CompleteScheduledActivityAsync(context, _payAction); Assert.Equal("Paid", context.GetProperty(CurrentStateProperty)); Assert.True(context.HasScheduledActivity(_paidEntry)); } [Fact(DisplayName = "StateMachine schedules event triggers when triggerless transition conditions are false")] public async Task FalseTriggerlessConditionAllowsTriggeredTransitions() { var payTransition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); payTransition.Trigger = null; payTransition.Condition = new(false); var context = await ExecuteAsync(); await CompleteScheduledActivityAsync(context, _newEntry); Assert.Equal("New", _stateMachine.CurrentState); Assert.False(context.HasScheduledActivity(_newExit)); Assert.True(context.HasScheduledActivity(_cancelTrigger)); Assert.Equal(ActivityStatus.Running, context.Status); } [Fact(DisplayName = "StateMachine leaves an all-false triggerless state active without rescheduling")] public async Task AllFalseTriggerlessTransitionsDoNotCompleteOrSpin() { _stateMachine.Transitions.Remove(_stateMachine.Transitions.Single(x => x.Name == "Cancel")); var payTransition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); payTransition.Trigger = null; payTransition.Condition = new(false); var context = await ExecuteAsync(); await CompleteScheduledActivityAsync(context, _newEntry); Assert.Equal("New", _stateMachine.CurrentState); Assert.Equal(ActivityStatus.Running, context.Status); Assert.False(context.HasScheduledActivity(_newExit)); Assert.False(context.HasScheduledActivity(_payAction)); Assert.False(context.HasScheduledActivity(_paidEntry)); Assert.DoesNotContain(context.WorkflowExecutionContext.CompletionCallbacks, x => x.Owner == context); } [Fact(DisplayName = "StateMachine completes after entering a terminal state")] public async Task TerminalStateCompletesStateMachine() { var stateMachine = new StateMachineActivity { InitialState = "Done", States = { new StateMachineState { Name = "Done" } } }; var context = await ExecuteAsync(stateMachine); Assert.Equal("Done", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Completed, context.Status); Assert.Empty(context.WorkflowExecutionContext.Scheduler.List()); } [Fact(DisplayName = "StateMachine accepts the first eligible triggerless transition in declaration order")] public async Task TriggerlessTransitionsUseDeclarationOrder() { var stateMachine = new StateMachineActivity { InitialState = "Source", States = { new StateMachineState { Name = "Source" }, new StateMachineState { Name = "First" }, new StateMachineState { Name = "Second" } }, Transitions = { new Transition { Name = "FirstTransition", From = "Source", To = "First", Condition = new(true) }, new Transition { Name = "SecondTransition", From = "Source", To = "Second", Condition = new(true) } } }; var context = await ExecuteAsync(stateMachine); Assert.Equal("First", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Completed, context.Status); } [Fact(DisplayName = "StateMachine yields an empty triggerless self-cycle to the workflow scheduler")] public async Task EmptyTriggerlessSelfCycleYieldsToScheduler() { var stateMachine = new StateMachineActivity { InitialState = "A", States = { new StateMachineState { Name = "A" } }, Transitions = { new Transition { From = "A", To = "A" } } }; var context = await ExecuteAsync(stateMachine); Assert.Equal("A", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Running, context.Status); var continuation = context.WorkflowExecutionContext.Scheduler.Take(); await CompleteScheduledActivityAsync(context, continuation.Activity); Assert.Equal("A", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Running, context.Status); Assert.Single(context.WorkflowExecutionContext.Scheduler.List()); } [Fact(DisplayName = "StateMachine yields an empty triggerless two-state cycle to the workflow scheduler")] public async Task EmptyTriggerlessTwoStateCycleYieldsToScheduler() { var stateMachine = new StateMachineActivity { InitialState = "A", States = { new StateMachineState { Name = "A" }, new StateMachineState { Name = "B" } }, Transitions = { new Transition { From = "A", To = "B" }, new Transition { From = "B", To = "A" } } }; var context = await ExecuteAsync(stateMachine); Assert.Equal("B", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Running, context.Status); var continuation = context.WorkflowExecutionContext.Scheduler.Take(); await CompleteScheduledActivityAsync(context, continuation.Activity); Assert.Equal("A", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Running, context.Status); Assert.Single(context.WorkflowExecutionContext.Scheduler.List()); } [Fact(DisplayName = "StateMachine resumes an automatic cycle continuation after state persistence")] public async Task AutomaticCycleContinuationSurvivesStateRoundTrip() { var stateMachine = new StateMachineActivity { InitialState = "A", States = { new StateMachineState { Name = "A" } }, Transitions = { new Transition { From = "A", To = "A" } } }; var fixture = CreatePersistenceFixture(stateMachine); var context = await fixture.BuildAsync(); context.Id = "state-machine-context"; context.WorkflowExecutionContext.AddActivityExecutionContext(context); await fixture.ExecuteAsync(context); var sourceWorkflowContext = context.WorkflowExecutionContext; var sourceContinuation = Assert.Single(sourceWorkflowContext.Scheduler.List()); Assert.Equal(context.Id, sourceContinuation.SchedulingActivityExecutionId); Assert.Single(sourceWorkflowContext.CompletionCallbacks); var extractor = sourceWorkflowContext.GetRequiredService(); var state = extractor.Extract(sourceWorkflowContext); var resumedWorkflowContext = await WorkflowExecutionContext.CreateAsync( sourceWorkflowContext.ServiceProvider, sourceWorkflowContext.WorkflowGraph, state.Id, CancellationToken.None); await extractor.ApplyAsync(resumedWorkflowContext, state); var resumedStateMachineContext = Assert.Single(resumedWorkflowContext.ActivityExecutionContexts, x => x.Activity == stateMachine); var resumedContinuation = resumedWorkflowContext.Scheduler.Take(); Assert.Single(state.CompletionCallbacks); Assert.Equal(resumedStateMachineContext.Id, resumedContinuation.Owner?.Id); Assert.Equal(resumedStateMachineContext.Id, resumedContinuation.SchedulingActivityExecutionId); Assert.Contains(resumedWorkflowContext.CompletionCallbacks, x => x.Owner == resumedStateMachineContext && x.Child.Activity == resumedContinuation.Activity); var resumedChildContext = await resumedWorkflowContext.CreateActivityExecutionContextAsync(resumedContinuation.Activity, new ActivityInvocationOptions { Owner = resumedContinuation.Owner, Tag = resumedContinuation.Tag, SchedulingActivityExecutionId = resumedContinuation.SchedulingActivityExecutionId }); resumedChildContext.TransitionTo(ActivityStatus.Running); resumedWorkflowContext.AddActivityExecutionContext(resumedChildContext); await resumedContinuation.Activity.ExecuteAsync(resumedChildContext); Assert.Equal("A", resumedStateMachineContext.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Running, resumedStateMachineContext.Status); Assert.Single(resumedWorkflowContext.Scheduler.List()); } [Fact(DisplayName = "StateMachine completes a triggerless transition with a composite action")] public async Task TriggerlessTransitionWithCompositeActionCompletes() { var stateMachine = new StateMachineActivity { InitialState = "Source", States = { new StateMachineState { Name = "Source", Exit = new WriteLine("exit") { Id = "source-exit" } }, new StateMachineState { Name = "Target" } }, Transitions = { new Transition { From = "Source", To = "Target", Condition = new(true), Action = new Sequence { Activities = { new WriteLine("first action") { Id = "first-action" }, new WriteLine("second action") { Id = "second-action" } } } } } }; var fixture = CreatePersistenceFixture(stateMachine); var context = await fixture.BuildAsync(); context.Id = "state-machine-context"; context.WorkflowExecutionContext.AddActivityExecutionContext(context); await fixture.ExecuteAsync(context); while (context.WorkflowExecutionContext.Scheduler.HasAny) await ExecuteNextScheduledActivityAsync(context.WorkflowExecutionContext); Assert.Equal("Target", context.GetProperty(CurrentStateProperty)); Assert.Equal(ActivityStatus.Completed, context.Status); } [Fact(DisplayName = "StateMachine self-transition executes exit, action and entry in order")] public async Task SelfTransitionExecutesExitActionAndEntryInOrder() { var transition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); transition.To = "New"; var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.True(context.HasScheduledActivity(_newExit)); Assert.False(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _newExit); Assert.True(context.HasScheduledActivity(_payAction)); await CompleteScheduledActivityAsync(context, _payAction); Assert.Equal("New", _stateMachine.CurrentState); Assert.True(context.HasScheduledActivity(_newEntry)); } [Fact(DisplayName = "StateMachine false transition condition leaves competing triggers active")] public async Task FalseConditionLeavesCompetingTriggersActive() { var context = await ExecuteAndEnterNewStateAsync(); _stateMachine.Transitions.Single(x => x.Name == "Pay").Condition = new(false); var cancelTriggerContext = await CreateScheduledActivityContextAsync(context, _cancelTrigger); var cancelBookmark = cancelTriggerContext.CreateBookmark("cancel"); var scheduledPayTriggerCount = CountScheduledActivities(context, _payTrigger); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.Equal("New", _stateMachine.CurrentState); Assert.False(context.HasScheduledActivity(_payAction)); Assert.Equal(scheduledPayTriggerCount + 1, CountScheduledActivities(context, _payTrigger)); Assert.NotEqual(ActivityStatus.Canceled, cancelTriggerContext.Status); Assert.Contains(cancelBookmark, context.WorkflowExecutionContext.Bookmarks); } [Fact(DisplayName = "StateMachine cancels competing outbound triggers when a transition wins")] public async Task AcceptedTransitionCancelsCompetingOutboundTriggers() { var context = await ExecuteAndEnterNewStateAsync(); var cancelTriggerContext = await CreateScheduledActivityContextAsync(context, _cancelTrigger); var cancelBookmark = cancelTriggerContext.CreateBookmark("cancel"); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.Equal(ActivityStatus.Canceled, cancelTriggerContext.Status); Assert.DoesNotContain(cancelBookmark, context.WorkflowExecutionContext.Bookmarks); } [Fact(DisplayName = "StateMachine cancels every distinct competing outbound trigger")] public async Task AcceptedTransitionCancelsDistinctCompetingOutboundTriggers() { var thirdTrigger = new WriteLine("third trigger") { Id = "third-trigger" }; _stateMachine.Transitions.Add(new Transition { Name = "CloseFromNew", From = "New", To = "Closed", Trigger = thirdTrigger }); var context = await ExecuteAndEnterNewStateAsync(); var cancelTriggerContext = await CreateScheduledActivityContextAsync(context, _cancelTrigger); var thirdTriggerContext = await CreateScheduledActivityContextAsync(context, thirdTrigger); var cancelBookmark = cancelTriggerContext.CreateBookmark("cancel"); var thirdBookmark = thirdTriggerContext.CreateBookmark("third"); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.Equal(ActivityStatus.Canceled, cancelTriggerContext.Status); Assert.Equal(ActivityStatus.Canceled, thirdTriggerContext.Status); Assert.DoesNotContain(cancelBookmark, context.WorkflowExecutionContext.Bookmarks); Assert.DoesNotContain(thirdBookmark, context.WorkflowExecutionContext.Bookmarks); } [Fact(DisplayName = "StateMachine rejects transitions that share a trigger instance")] public async Task SharedTriggerInstanceIsRejected() { _stateMachine.Transitions.Single(x => x.Name == "Cancel").Trigger = _payTrigger; var exception = await Assert.ThrowsAsync(() => ExecuteAsync()); Assert.Contains("cannot share a Trigger activity", exception.Message); } [Fact(DisplayName = "StateMachine rejects transition triggers with duplicate activity IDs")] public async Task DuplicateTriggerIdIsRejected() { _cancelTrigger.Id = _payTrigger.Id; var exception = await Assert.ThrowsAsync(() => ExecuteAsync()); Assert.Contains("unique ID", exception.Message); } [Fact(DisplayName = "StateMachine removes re-armed competing triggers when a different transition wins")] public async Task AcceptedTransitionRemovesRearmedCompetingTriggers() { var context = await ExecuteAndEnterNewStateAsync(); _stateMachine.Transitions.Single(x => x.Name == "Pay").Condition = new(false); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.True(context.WorkflowExecutionContext.Scheduler.Any(x => x.Activity == _payTrigger)); Assert.Contains(context.WorkflowExecutionContext.CompletionCallbacks, x => x.Owner == context && x.Child.Activity == _payTrigger); await CompleteScheduledActivityAsync(context, _cancelTrigger); Assert.False(context.WorkflowExecutionContext.Scheduler.Any(x => x.Activity == _payTrigger)); Assert.DoesNotContain(context.WorkflowExecutionContext.CompletionCallbacks, x => x.Owner == context && x.Child.Activity == _payTrigger); } [Fact(DisplayName = "StateMachine stores current state in the activity execution context")] public async Task StoresCurrentStateInActivityExecutionContext() { var payTransition = _stateMachine.Transitions.Single(x => x.Name == "Pay"); payTransition.Action = null; _stateMachine.States.Single(x => x.Name == "New").Exit = null; _stateMachine.States.Single(x => x.Name == "Paid").Entry = null; var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); Assert.Equal("Paid", context.GetProperty(CurrentStateProperty)); } [Fact(DisplayName = "StateMachine resolves duplicate unnamed transition endpoints by scheduled transition")] public async Task ResolvesDuplicateUnnamedTransitionEndpointsByScheduledTransition() { var firstTrigger = new WriteLine("first trigger") { Id = "first-trigger" }; var secondTrigger = new WriteLine("second trigger") { Id = "second-trigger" }; var firstAction = new WriteLine("first action") { Id = "first-action" }; var secondAction = new WriteLine("second action") { Id = "second-action" }; var stateMachine = new StateMachineActivity { InitialState = "New", States = { new StateMachineState { Name = "New" }, new StateMachineState { Name = "Paid" } }, Transitions = { new Transition { From = "New", To = "Paid", Trigger = firstTrigger, Action = firstAction }, new Transition { From = "New", To = "Paid", Trigger = secondTrigger, Action = secondAction } } }; var context = await ExecuteAsync(stateMachine); await CompleteScheduledActivityAsync(context, secondTrigger); Assert.False(context.HasScheduledActivity(firstAction)); Assert.True(context.HasScheduledActivity(secondAction)); } [Fact(DisplayName = "StateMachine resolves duplicate named transitions by current state")] public async Task ResolvesDuplicateNamedTransitionsByCurrentState() { var payTrigger = new WriteLine("pay trigger") { Id = "pay-duplicate-trigger" }; var newCancelTrigger = new WriteLine("new cancel trigger") { Id = "new-cancel-trigger" }; var paidCancelTrigger = new WriteLine("paid cancel trigger") { Id = "paid-cancel-trigger" }; var stateMachine = new StateMachineActivity { InitialState = "New", States = { new StateMachineState { Name = "New" }, new StateMachineState { Name = "Paid" }, new StateMachineState { Name = "Closed" } }, Transitions = { new Transition { Name = "Cancel", From = "New", To = "Closed", Trigger = newCancelTrigger }, new Transition { Name = "Pay", From = "New", To = "Paid", Trigger = payTrigger }, new Transition { Name = "Cancel", From = "Paid", To = "Closed", Trigger = paidCancelTrigger } } }; var context = await ExecuteAsync(stateMachine); await CompleteScheduledActivityAsync(context, payTrigger); await CompleteScheduledActivityAsync(context, paidCancelTrigger); Assert.Equal("Closed", context.GetProperty(CurrentStateProperty)); } [Fact(DisplayName = "StateMachine ignores stale transition action completions")] public async Task IgnoresStaleTransitionActionCompletions() { var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); await CompleteScheduledActivityAsync(context, _newExit); context.SetProperty(CurrentStateProperty, "Paid"); await CompleteScheduledActivityAsync(context, _payAction); Assert.False(context.HasScheduledActivity(_paidEntry)); } [Fact(DisplayName = "StateMachine ignores stale state exit completions")] public async Task IgnoresStaleStateExitCompletions() { var context = await ExecuteAndEnterNewStateAsync(); await CompleteScheduledActivityAsync(context, _payTrigger); context.SetProperty(CurrentStateProperty, "Paid"); await CompleteScheduledActivityAsync(context, _newExit); Assert.False(context.HasScheduledActivity(_payAction)); } private async Task ExecuteAndEnterNewStateAsync() { var context = await ExecuteAsync(); await CompleteScheduledActivityAsync(context, _newEntry); return context; } private static ActivityTestFixture CreateFixture(StateMachineActivity stateMachine) => new ActivityTestFixture(stateMachine) .ConfigureServices(services => { services.RemoveAll(); services.AddSingleton(); }); private static ActivityTestFixture CreatePersistenceFixture(StateMachineActivity stateMachine) => CreateFixture(stateMachine) .ConfigureServices(services => { services.RemoveAll(); services.AddSingleton(); services.AddSingleton(_ => Substitute.For()); services.AddSingleton(_ => Substitute.For()); }); private Task ExecuteAsync(StateMachineActivity? stateMachine = null) => CreateFixture(stateMachine ?? _stateMachine).ExecuteAsync(); private static async Task CompleteScheduledActivityAsync(ActivityExecutionContext ownerContext, IActivity activity) { var childContext = await CreateScheduledActivityContextAsync(ownerContext, activity); var callback = PopCallback(ownerContext, activity); Assert.NotNull(callback?.CompletionCallback); ownerContext.Tag = callback!.Tag; await callback.CompletionCallback!(new ActivityCompletedContext(ownerContext, childContext)); } private static async Task CreateScheduledActivityContextAsync(ActivityExecutionContext ownerContext, IActivity activity) { var callback = ownerContext.WorkflowExecutionContext.CompletionCallbacks.LastOrDefault(x => x.Owner == ownerContext && x.Child.Activity == activity); var childContext = await ownerContext.WorkflowExecutionContext.CreateActivityExecutionContextAsync(activity, new ActivityInvocationOptions { Owner = ownerContext, Tag = callback?.Tag }); childContext.TransitionTo(ActivityStatus.Running); ownerContext.WorkflowExecutionContext.AddActivityExecutionContext(childContext); return childContext; } private static async Task ExecuteNextScheduledActivityAsync(WorkflowExecutionContext workflowContext) { var workItem = workflowContext.Scheduler.Take(); var childContext = await workflowContext.CreateActivityExecutionContextAsync(workItem.Activity, new ActivityInvocationOptions { Owner = workItem.Owner, ExistingActivityExecutionContext = workItem.ExistingActivityExecutionContext, Tag = workItem.Tag, Variables = workItem.Variables, Input = workItem.Input, SchedulingActivityExecutionId = workItem.SchedulingActivityExecutionId, SchedulingWorkflowInstanceId = workItem.SchedulingWorkflowInstanceId, SchedulingCallStackDepth = workItem.SchedulingCallStackDepth }); childContext.TransitionTo(ActivityStatus.Running); workflowContext.AddActivityExecutionContext(childContext); await childContext.Activity.ExecuteAsync(childContext); } private static int CountScheduledActivities(ActivityExecutionContext context, IActivity activity) => context.WorkflowExecutionContext.Scheduler.List().Count(x => x.Activity == activity); private static ActivityCompletionCallbackEntry? PopCallback(ActivityExecutionContext ownerContext, IActivity activity) { var callback = ownerContext.WorkflowExecutionContext.CompletionCallbacks.LastOrDefault(x => x.Owner == ownerContext && x.Child.Activity == activity); if (callback != null) ownerContext.WorkflowExecutionContext.RemoveCompletionCallback(callback); return callback; } }