public async Task <OpenApiResult> HandleTrigger(IOpenApiContext context, IWorkflowTrigger body) { ITenant tenant = await this.marainServicesTenancy.GetRequestingTenantAsync(context.CurrentTenantId).ConfigureAwait(false); string delegatedTenantId = await this.marainServicesTenancy.GetDelegatedTenantIdForRequestingTenantAsync(tenant.Id).ConfigureAwait(false); var operationId = Guid.NewGuid(); CreateOperationHeaders operationHeaders = await this.operationsControl.CreateOperationAsync(delegatedTenantId, operationId).ConfigureAwait(false); var envelope = new WorkflowMessageEnvelope(this.propertyBagFactory.Create(PropertyBagValues.Empty)) { Trigger = body, OperationId = operationId, TenantId = context.CurrentTenantId, }; var durableFunctionsOpenApiContext = (DurableFunctionsOpenApiContext)context; await durableFunctionsOpenApiContext.OrchestrationClient.StartNewWithCustomSerializationSettingsAsync( nameof(TriggerExecutionOrchestrator), operationId.ToString(), envelope, this.serializerSettingsProvider.Instance).ConfigureAwait(false); return(this.AcceptedResult(operationHeaders.Location)); }
/// <inheritdoc /> public Task ExecuteAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { #pragma warning disable CA2254 // Template should be a static expression - the whole point of this action type is to allow user-defined log messages this.logger.LogDebug(BuildMessage(this.LogMessage, instance)); #pragma warning restore CA2254 return(Task.CompletedTask); }
/// <inheritdoc/> public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is HostedWorkflowTrigger hostedTrigger) { string actualValue = null; bool parameterExists = hostedTrigger.Parameters?.TryGetValue(this.ParameterName, out actualValue) ?? false; switch (this.ParameterTestType) { case HostedWorkflowTriggerParameterTestType.Equals: return(Task.FromResult(parameterExists && actualValue == this.ParameterValue)); case HostedWorkflowTriggerParameterTestType.DoesNotEqual: return(Task.FromResult(parameterExists && actualValue != this.ParameterValue)); case HostedWorkflowTriggerParameterTestType.Exists: return(Task.FromResult(parameterExists)); case HostedWorkflowTriggerParameterTestType.DoesNotExist: return(Task.FromResult(!parameterExists)); } } return(Task.FromResult(false)); }
public async Task ExecuteAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is EditCatalogItemTrigger patchTrigger) { await this.ExecuteAsync(instance, patchTrigger.PatchDetails); } }
public Task ExecuteAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { var newTrigger = new CreateCatalogItemTrigger { CatalogItemId = instance.Id }; return(this.queue.EnqueueTriggerAsync(newTrigger, default(Guid))); }
public Task ExecuteAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { var log = GetTraces(); log.Add(this.Message); return(Task.CompletedTask); }
public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is ICatalogItemTrigger itemTrigger) { instance.Context["CatalogItemId"] = itemTrigger.CatalogItemId; } return(Task.FromResult(true)); }
public async Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is EditCatalogItemTrigger editTrigger) { return(await this.EvaluateAsync(instance, editTrigger.PatchDetails).ConfigureAwait(false)); } return(false); }
public async Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is ICatalogItemTrigger patchTrigger) { return(await this.EvaluateAsync(instance, patchTrigger.CatalogItemId)); } return(false); }
/// <summary> /// Executes all of the actions in the supplied collection against the given /// <see cref="WorkflowInstance" /> with the context of the specified trigger. /// </summary> /// <param name="actions">The list of <see cref="IWorkflowAction" />s to execute.</param> /// <param name="instance">The <see cref="WorkflowInstance" /> to execute the actions against.</param> /// <param name="trigger"> /// The <see cref="IWorkflowTrigger" /> that is being processed to cause these actions to be /// executed. It is possible for this parameter to be null if the actions being executed /// are the entry actions for the initial state of a new <see cref="WorkflowInstance" />. /// </param> /// <returns>A <see cref="Task" /> that will complete when all actions have been run.</returns> /// <remarks> /// The actions will be executed sequentially. If an action throws an /// exception, subsequent actions will not be executed. /// This is a helper method for internal use only. It is used to make code /// more readable when executing exit/transition/entry actions. /// </remarks> private async Task ExecuteAsync( IEnumerable <IWorkflowAction> actions, WorkflowInstance instance, IWorkflowTrigger trigger) { foreach (IWorkflowAction obj in actions) { await obj.ExecuteAsync(instance, trigger).ConfigureAwait(false); } }
/// <inheritdoc /> public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { if (trigger is HostedWorkflowTrigger hostedTrigger) { bool result = hostedTrigger.TriggerName == this.TriggerName; return(Task.FromResult(result)); } return(Task.FromResult(false)); }
/// <inheritdoc /> /// <exception cref="InvalidOperationException"> /// Thrown if the <see cref="FinishProcessing" /> method has been called. /// </exception> public Task EnqueueTriggerAsync( IWorkflowTrigger trigger, Guid operationId) { return(this.EnqueueMessageEnvelopeAsync( new WorkflowMessageEnvelope(this.propertyBagFactory.Create(PropertyBagValues.Empty)) { Trigger = trigger, })); }
public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { foreach (var current in this.RequiredContextItems) { if (!instance.Context.ContainsKey(current)) { return(Task.FromResult(false)); } } return(Task.FromResult(true)); }
/// <summary> /// The Process method is invoked asynchronously when <see cref="StartProcessing" /> is /// called. It will run on a background thread until <see cref="FinishProcessing" /> /// is called. /// </summary> /// <returns> /// A <see cref="Task" /> that will complete after <see cref="FinishProcessing" /> is called. /// </returns> private async Task Process() { while (true) { if (this.queue.IsEmpty) { if (this.shouldComplete) { break; } await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false); continue; } this.queue.TryPeek(out WorkflowMessageEnvelope item); IWorkflowInstanceStore instanceStore = await this.workflowInstanceStoreFactory.GetWorkflowInstanceStoreForTenantAsync(this.tenantProvider.Root).ConfigureAwait(false); IWorkflowEngine engine = await this.workflowEngineFactory.GetWorkflowEngineAsync(this.tenantProvider.Root).ConfigureAwait(false); if (item.IsTrigger) { this.logger.LogInformation("Processing trigger with content type {ContentType}", item.ContentType); IWorkflowTrigger trigger = item.Trigger; IEnumerable <string> instanceIds = await instanceStore.GetMatchingWorkflowInstanceIdsForSubjectsAsync( item.Trigger.GetSubjects(), int.MaxValue, 0).ConfigureAwait(false); foreach (string current in instanceIds) { await engine.ProcessTriggerAsync(trigger, current).ConfigureAwait(false); } } else { this.logger.LogInformation("Processing start workflow instance request"); await engine.StartWorkflowInstanceAsync(item.StartWorkflowInstanceRequest) .ConfigureAwait(false); } this.queue.TryDequeue(out _); } }
public async Task ExecuteAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { var item = new CatalogItem { Id = instance.Id, Identifier = instance.Context["Identifier"], Type = instance.Context["Type"] }; instance.Context.Remove("Identifier"); instance.Context.Remove("Type"); var repository = this.repositoryFactory.GetRepository(); await repository.UpsertItemAsync(item).ConfigureAwait(false); }
/// <inheritdoc /> public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { bool result = false; if (trigger is EntityIdTrigger etrigger) { if (instance.Context.TryGetValue(this.EntityIdContextProperty, out string value)) { result = value == etrigger.EntityId; } result = result && etrigger.Activity == this.Activity; } return(Task.FromResult(result)); }
/// <summary> /// Finds the first <see cref="WorkflowTransition"/> in the supplied /// list that can accept the specified <see cref="IWorkflowTrigger"/>. /// </summary> /// <param name="transitions">The transitions to evaluate.</param> /// <param name="instance">The <see cref="WorkflowInstance"/> to which the transition belongs.</param> /// <param name="trigger">The trigger that is currently being processed.</param> /// <returns> /// A <see cref="Task"/> whose result will be the first transition /// from the list that can accept the trigger. If none of the transitions /// can accept the trigger, null is returned. /// </returns> private async Task <WorkflowTransition> FindTransitionAsync( IEnumerable <WorkflowTransition> transitions, WorkflowInstance instance, IWorkflowTrigger trigger) { foreach (WorkflowTransition transition in transitions) { this.logger.LogDebug("Considering transition {TransitionId} [{TransitionDisplayName}]", transition.Id, transition.DisplayName); if (await this.CheckConditionsAsync(transition.Conditions, instance, trigger).ConfigureAwait(false)) { this.logger.LogDebug("Accepted transition {TransitionId} [{TransitionDisplayName}]", transition.Id, transition.DisplayName); return(transition); } this.logger.LogDebug("Rejected transition {TransitionId} [{TransitionDisplayName}]", transition.Id, transition.DisplayName); } return(null); }
/// <summary> /// Passes a trigger to an instance and returns the new state. /// </summary> /// <param name="instance"> /// The instance that should process the trigger. /// </param> /// <param name="trigger"> /// The trigger that should be processed. /// </param> /// <returns> /// A <see cref="Task"/> that completes when trigger processing has finished. /// </returns> /// <remarks> /// See the documentation for the <see cref="AcceptTriggerAsync(Workflow, WorkflowState, WorkflowInstance, IWorkflowTrigger)"/> /// for a full explanation of the processing steps. /// </remarks> private async Task <WorkflowTransition> AcceptTriggerAsync( WorkflowInstance instance, IWorkflowTrigger trigger) { Workflow workflow = await this.workflowStore.GetWorkflowAsync(instance.WorkflowId).ConfigureAwait(false); WorkflowState state = workflow.GetState(instance.StateId); if (instance.Status == WorkflowStatus.Faulted) { return(null); } if (instance.Status == WorkflowStatus.Complete) { return(null); } return(await this.AcceptTriggerAsync(workflow, state, instance, trigger).ConfigureAwait(false)); }
/// <summary> /// Evaluates all of the conditions in the supplied collection against the given /// <see cref="WorkflowInstance" /> with the context of the specified trigger. /// </summary> /// <param name="conditions">The list of <see cref="IWorkflowCondition" />s to evaluate.</param> /// <param name="instance">The <see cref="WorkflowInstance" /> to evaluate the conditions actions against.</param> /// <param name="trigger"> /// The <see cref="IWorkflowTrigger" /> that is being processed to cause these conditions to be /// evaluated. It is possible for this parameter to be null if the conditions being evaluated /// are the entry conditions for the initial state of a new <see cref="WorkflowInstance" />. /// </param> /// <returns>A <see cref="Task" /> that will complete when all conditions have been evaluated.</returns> /// <remarks> /// The actions will be executed sequentially. If a condition is not met, remaining /// conditions will not be evaluated. /// This is a helper method for internal use only. It is used to make code /// more readable when evaluating exit/transition/entry conditions. /// </remarks> private async Task <bool> CheckConditionsAsync( IEnumerable <IWorkflowCondition> conditions, WorkflowInstance instance, IWorkflowTrigger trigger) { foreach (IWorkflowCondition condition in conditions) { string conditionName = condition.GetType().Name; this.logger.LogDebug("Evaluating condition {ConditionId} {ConditionName}", condition.Id, conditionName); if (!await condition.EvaluateAsync(instance, trigger).ConfigureAwait(false)) { this.logger.LogDebug("Condition false: {ConditionId} {ConditionName}", condition.Id, conditionName); return(false); } this.logger.LogDebug("Condition true: {ConditionId} {ConditionName}", condition.Id, conditionName); } return(true); }
/// <summary> /// Initializes a new instance of the <see cref="WorkflowInstanceTransitionCloudEventData"/> class. /// </summary> /// <param name="workflowInstanceId">The <see cref="WorkflowInstanceId"/>.</param> /// <param name="trigger">The (optional) <see cref="Trigger"/>.</param> public WorkflowInstanceTransitionCloudEventData(string workflowInstanceId, IWorkflowTrigger trigger = null) { this.WorkflowInstanceId = workflowInstanceId; this.Trigger = trigger; }
/// <inheritdoc /> public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { bool result = trigger.ContentType == this.TriggerContentType; return(Task.FromResult(result)); }
/// <inheritdoc/> public Task ProcessTriggerAsync(IWorkflowTrigger trigger, string workflowInstanceId, string partitionKey = null) { return(this.ProcessInstanceWithLeaseAsync(trigger, workflowInstanceId, partitionKey)); }
public async Task <OpenApiResult> HandleTrigger(string tenantId, string workflowInstanceId, IWorkflowTrigger body) { ITenant tenant = await this.marainServicesTenancy.GetRequestingTenantAsync(tenantId).ConfigureAwait(false); IWorkflowEngine workflowEngine = await this.workflowEngineFactory.GetWorkflowEngineAsync(tenant).ConfigureAwait(false); await workflowEngine.ProcessTriggerAsync(body, workflowInstanceId).ConfigureAwait(false); return(this.OkResult()); }
/// <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); } } }
/// <summary> /// Wraps the <see cref="ProcessInstanceAsync" /> method to use the current <see cref="leaseProvider" /> /// to take a lease on the workflow instance prior to processing. /// </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> /// If another instance of WorkflowEngine already has a lease on the WorkflowInstance, /// this method will wait for the lease to become available. /// </remarks> private Task ProcessInstanceWithLeaseAsync(IWorkflowTrigger trigger, string instanceId, string partitionKey) { return(this.leaseProvider .ExecuteWithMutexAsync(_ => this.ProcessInstanceAsync(trigger, instanceId, partitionKey), instanceId)); }
public Task <bool> EvaluateAsync(WorkflowInstance instance, IWorkflowTrigger trigger) { return(Task.FromResult(trigger.Id == this.TriggerId)); }
/// <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); }