/// <summary> /// Determines whether an <see cref="WorkflowState" /> is able to accept a trigger, /// and processes it if so. /// </summary> /// <param name="workflow">The workflow that is in operation.</param> /// <param name="state">The state that will process the trigger.</param> /// <param name="instance">The <see cref="WorkflowInstance" /> that is in this state.</param> /// <param name="trigger">The <see cref="IWorkflowTrigger" /> to process.</param> /// <returns> /// A <see cref="Task" /> that will complete when the trigger has been /// processed, or it is determined that the trigger can't be processed /// for the given <see cref="WorkflowInstance" /> and <see cref="WorkflowState" />. /// </returns> /// <remarks> /// <para> /// This method consists of three parts: /// <list type="bullet"> /// <item> /// <description> /// Determining if the supplied trigger can be accepted by the /// <see cref="WorkflowInstance" /> in its current state. /// </description> /// </item> /// <item> /// <description> /// Executing the actions required to move to the new state. /// </description> /// </item> /// <item> /// <description> /// Updating the <see cref="WorkflowInstance" /> with the new state and /// associated interests. /// </description> /// </item> /// </list> /// </para> /// <para> /// To determine the transition to use, the following steps are taken: /// <list type="bullet"> /// <item> /// <description> /// Evaluate the exit conditions of the current state (from /// <see cref="WorkflowState.ExitConditions" />. If any conditions /// evaluate to false, processing ends. /// </description> /// </item> /// <item> /// <description> /// Iterate the <see cref="WorkflowState.Transitions" /> collection /// and select the first <see cref="WorkflowTransition" /> whose /// <see cref="WorkflowTransition.Conditions" /> all evaluate to true. /// If no transitions match, then processing ends. /// </description> /// </item> /// <item> /// <description> /// Retrieve the target state from the transition, and evaluate its /// entry conditions (from <see cref="WorkflowState.EntryConditions" />. /// If any conditions evaluate to false, processing ends. /// </description> /// </item> /// </list> /// </para> /// <para> /// Once it has been determined that the trigger can be processed, actions /// from the current state, transition and target state are executed in order: /// <list type="bullet"> /// <item> /// <description> /// The current state's <see cref="WorkflowState.ExitActions" />. /// </description> /// </item> /// <item> /// <description> /// The transition's <see cref="WorkflowTransition.Actions" /> /// </description> /// </item> /// <item> /// <description> /// The target state's <see cref="WorkflowState.ExitActions" />. /// </description> /// </item> /// </list> /// </para> /// <para> /// Once all actions have been processed, the <see cref="WorkflowInstance" />s status /// is set back to <see cref="WorkflowStatus.Waiting" />, if the new current state /// contains any transitions. If the new current state doesn't contain any transitions, /// there is no way of leaving this state and the workflow status is set to /// <see cref="WorkflowStatus.Complete" />. Additionally, the <see cref="WorkflowInstance.IsDirty" /> /// property is set to true to ensure that the instance is saved by the <see cref="IWorkflowEngine" />. /// </para> /// </remarks> private async Task <WorkflowTransition> AcceptTriggerAsync( Workflow workflow, WorkflowState state, WorkflowInstance instance, IWorkflowTrigger trigger) { bool exitAllowed = await this.CheckConditionsAsync(state.ExitConditions, instance, trigger).ConfigureAwait(false); if (!exitAllowed) { this.logger.LogDebug( "Exit not permitted from state {StateId} [{StateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", state.Id, state.DisplayName, instance.Id, trigger.Id); instance.Status = WorkflowStatus.Waiting; return(null); } WorkflowTransition transition = await this.FindTransitionAsync(state.Transitions, instance, trigger).ConfigureAwait(false); if (transition == null) { this.logger.LogDebug( "No transition found from state {StateId} [{StateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", state.Id, state.DisplayName, instance.Id, trigger.Id); instance.Status = WorkflowStatus.Waiting; return(transition); } WorkflowState targetState = workflow.GetState(transition.TargetStateId); this.logger.LogDebug( "Transition {TransitionId} [{TransitionDisplayName}] found from state {StateId} [{StateDisplayName}] to state {TargetStateId} [{TargetStateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", transition.Id, transition.DisplayName, state.Id, state.DisplayName, targetState.Id, targetState.DisplayName, instance.Id, trigger.Id); bool entryAllowed = await this.CheckConditionsAsync(targetState.EntryConditions, instance, trigger) .ConfigureAwait(false); if (!entryAllowed) { this.logger.LogDebug( "Entry not permitted into state {TargetStateId} [{TargetStateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", targetState.Id, targetState.DisplayName, instance.Id, trigger.Id); instance.Status = WorkflowStatus.Waiting; return(transition); } this.logger.LogDebug( "Executing exit actions on transition {TransitionId} from state {StateId} [{StateDisplayName}] to state {TargetStateId} [{TargetStateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", transition.Id, state.Id, state.DisplayName, targetState.Id, targetState.DisplayName, instance.Id, trigger.Id); await this.ExecuteAsync(state.ExitActions, instance, trigger).ConfigureAwait(false); this.logger.LogDebug( "Executing transition actions on transition {TransitionId} from state {StateId} [{StateDisplayName}] to state {TargetStateId} [{TargetStateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", transition.Id, state.Id, state.DisplayName, targetState.Id, targetState.DisplayName, instance.Id, trigger.Id); await this.ExecuteAsync(transition.Actions, instance, trigger).ConfigureAwait(false); instance.Status = targetState.Transitions.Count == 0 ? WorkflowStatus.Complete : WorkflowStatus.Waiting; instance.SetState(targetState); this.logger.LogDebug( "Executing entry actions on transition {TransitionId} from state {StateId} [{StateDisplayName}] to state {TargetStateId} [{TargetStateDisplayName}] in instance {InstanceId} with trigger {TriggerId}", transition.Id, state.Id, state.DisplayName, targetState.Id, targetState.DisplayName, instance.Id, trigger.Id); await this.ExecuteAsync(targetState.EntryActions, instance, trigger).ConfigureAwait(false); // Then update the instance status, set the new state instance.IsDirty = true; return(transition); }
/// <summary> /// Retrieves a single <see cref="WorkflowInstance" /> and passes it the trigger /// to process. /// </summary> /// <param name="trigger">The trigger to process.</param> /// <param name="instanceId">The Id of the <see cref="WorkflowInstance" /> that will process the trigger.</param> /// <param name="partitionKey">The partition key for the instance. If not supplied, the Id will be used.</param> /// <returns>A <see cref="Task" /> that will complete when the instance has finished processing the trigger.</returns> /// <remarks> /// This method retrieves the workflow instance from storage, passes it the trigger /// and, if the instance has updated as a result of the trigger, puts it back in /// storage. /// </remarks> private async Task ProcessInstanceAsync(IWorkflowTrigger trigger, string instanceId, string partitionKey) { WorkflowInstance item = null; Workflow workflow = null; // We need to gather data for the final CloudEvent prior to applying the transition, as some of the data // we publish is pre-transition - e.g. previous state and context. var workflowEventData = new WorkflowInstanceTransitionCloudEventData(instanceId, trigger); string workflowEventType = WorkflowEventTypes.TransitionCompleted; try { item = await this.workflowInstanceStore.GetWorkflowInstanceAsync(instanceId, partitionKey).ConfigureAwait(false); workflow = await this.workflowStore.GetWorkflowAsync(item.WorkflowId).ConfigureAwait(false); workflowEventData.PreviousContext = item.Context.ToDictionary(x => x.Key, x => x.Value); workflowEventData.WorkflowId = item.WorkflowId; workflowEventData.PreviousState = item.StateId; workflowEventData.PreviousStatus = item.Status; this.logger.LogDebug("Accepting trigger {TriggerId} in instance {ItemId}", trigger.Id, item.Id); WorkflowTransition transition = await this.AcceptTriggerAsync(item, trigger).ConfigureAwait(false); workflowEventData.TransitionId = transition?.Id; workflowEventData.NewState = item.StateId; workflowEventData.NewStatus = item.Status; workflowEventData.NewContext = item.Context; this.logger.LogDebug("Accepted trigger {TriggerId} in instance {ItemId}", trigger.Id, item.Id); } catch (WorkflowInstanceNotFoundException) { // Bubble this specific exception out as the caller needs to know that they sent through an // invalid workflow instance id. this.logger.LogError( new EventId(0), "Unable to locate the specified instance {InstanceId} for trigger {TriggerId}", instanceId, trigger.Id); throw; } catch (Exception ex) { this.logger.LogError( new EventId(0), ex, "Error accepting trigger {TriggerId} in instance {ItemId}", trigger.Id, item?.Id); if (item != null) { item.Status = WorkflowStatus.Faulted; item.IsDirty = true; workflowEventData.NewStatus = item.Status; workflowEventType = WorkflowEventTypes.InstanceFaulted; } } finally { if (item?.IsDirty == true) { await this.workflowInstanceStore.UpsertWorkflowInstanceAsync(item, partitionKey).ConfigureAwait(false); await this.cloudEventPublisher.PublishWorkflowEventDataAsync( this.cloudEventSource, workflowEventType, item.Id, workflowEventData.ContentType, workflowEventData, workflow.WorkflowEventSubscriptions).ConfigureAwait(false); } } }