diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs index 1c40937ea..078be3bea 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityCompletedContext.cs @@ -1,3 +1,6 @@ +using System.Diagnostics.CodeAnalysis; +using Elsa.Workflows.Activities.Flowchart.Models; + namespace Elsa.Workflows; /// @@ -21,4 +24,19 @@ public record ActivityCompletedContext(ActivityExecutionContext TargetContext, A /// A cancellation token to use when invoking asynchronous operations. /// public CancellationToken CancellationToken => WorkflowExecutionContext.CancellationTokens.ApplicationCancellationToken; + + /// + /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public async ValueTask CompleteActivityAsync(object? result = default) + { + await TargetContext.CompleteActivityAsync(result); + } + + /// + /// Complete the current activity with the specified outcomes. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public ValueTask CompleteActivityWithOutcomesAsync(params string[] outcomes) => CompleteActivityAsync(new Outcomes(outcomes)); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs index 082935672..808577679 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Cancel.cs @@ -19,9 +19,17 @@ public partial class ActivityExecutionContext _ = Task.Run(async () => await CancelActivityAsync()); } + + private bool CanCancelActivity() + { + return Status is not ActivityStatus.Canceled and not ActivityStatus.Completed; + } private async Task CancelActivityAsync() { + if(!CanCancelActivity()) + return; + // Select all child contexts. var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this).ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs new file mode 100644 index 000000000..f3963ed91 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using Elsa.Extensions; +using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Signals; + +namespace Elsa.Workflows; + +public partial class ActivityExecutionContext +{ + /// + /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public async ValueTask CompleteActivityAsync(object? result = default) + { + var outcomes = result as Outcomes; + + // If the activity is executing in the background, simply capture the result and return. + if (this.GetIsBackgroundExecution()) + { + if (outcomes != null) + this.SetBackgroundOutcomes(outcomes.Names); + else + this.SetBackgroundCompletion(); + return; + } + + // If the activity is not running, do nothing. + if (Status != ActivityStatus.Running) + return; + + // Cancel any non-completed child activities. + var childContexts = WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == this && x.CanCancelActivity()).ToList(); + + foreach (var childContext in childContexts) + await childContext.CancelActivityAsync(); + + // Mark the activity as complete. + TransitionTo(ActivityStatus.Completed); + + // Record the outcomes, if any. + if (outcomes != null) + JournalData["Outcomes"] = outcomes.Names; + + // Record the output, if any. + var activity = Activity; + var expressionExecutionContext = ExpressionExecutionContext; + var activityDescriptor = ActivityDescriptor; + var outputDescriptors = activityDescriptor.Outputs; + var outputs = outputDescriptors.ToDictionary(x => x.Name, x => activity.GetOutput(expressionExecutionContext, x.Name)!); + var serializer = GetRequiredService(); + + foreach (var outputDescriptor in outputDescriptors) + { + if (outputDescriptor.IsSerializable == false) + continue; + + var outputName = outputDescriptor.Name; + var outputValue = outputs[outputName]; + + if (outputValue == null!) + continue; + + var serializedOutputValue = await serializer.SerializeAsync(outputValue, CancellationToken); + JournalData[outputName] = serializedOutputValue; + } + + // Add an execution log entry. + AddExecutionLogEntry("Completed", payload: JournalData); + + // Send a signal. + await this.SendSignalAsync(new ActivityCompleted(result)); + + // Clear bookmarks. + ClearBookmarks(); + WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityInstanceId == Id); + + // Remove completion callbacks. + ClearCompletionCallbacks(); + + // Remove all associated variables, unless this is the root context - in which case we want to keep the variables since we're not deleting that one. + if (ParentActivityExecutionContext != null) + { + var variablePersistenceManager = GetRequiredService(); + await variablePersistenceManager.DeleteVariablesAsync(this); + } + + // Update the completed at timestamp. + CompletedAt = WorkflowExecutionContext.SystemClock.UtcNow; + } + + /// + /// Complete the current activity with the specified outcomes. + /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] + public ValueTask CompleteActivityWithOutcomesAsync(params string[] outcomes) + { + return CompleteActivityAsync(new Outcomes(outcomes)); + } + + /// + /// Complete the current composite activity with the specified outcome. + /// + public async ValueTask CompleteCompositeAsync(params string[] outcomes) + { + await this.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes))); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 2fc1d4d94..2be9c0b3d 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -136,13 +136,11 @@ public partial class ActivityExecutionContext : IExecutionContext public void TransitionTo(ActivityStatus status) { Status = status; - - if (Status is ActivityStatus.Completed - or ActivityStatus.Canceled - or ActivityStatus.Faulted) + + if (Status is ActivityStatus.Completed or ActivityStatus.Canceled or ActivityStatus.Faulted) _cancellationRegistration.Dispose(); } - + /// /// Gets or sets the exception that occurred during the activity execution, if any. /// @@ -254,15 +252,17 @@ public partial class ActivityExecutionContext : IExecutionContext { ActivityNodeId = activityNode?.NodeId, OwnerActivityInstanceId = owner?.Id, - Options = options != null ? new ScheduledActivityOptions - { - CompletionCallback = options?.CompletionCallback?.Method.Name, - Tag = options?.Tag, - ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id, - PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false, - Variables = options?.Variables?.ToList(), - Input = options?.Input - } : default + Options = options != null + ? new ScheduledActivityOptions + { + CompletionCallback = options?.CompletionCallback?.Method.Name, + Tag = options?.Tag, + ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id, + PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false, + Variables = options?.Variables?.ToList(), + Input = options?.Input + } + : default }; var scheduledActivities = this.GetBackgroundScheduledActivities().ToList(); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 4d934abcf..5bcc6f3c1 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using System.Text.Json; @@ -6,7 +7,6 @@ using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Mediator.Contracts; using Elsa.Workflows; -using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.Attributes; using Elsa.Workflows.Contracts; using Elsa.Workflows.Memory; @@ -406,95 +406,7 @@ public static class ActivityExecutionContextExtensions /// /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. /// - public static async ValueTask CompleteActivityAsync(this ActivityCompletedContext context, object? result = default) - { - await context.TargetContext.CompleteActivityAsync(result); - } - - /// - /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. - /// - public static async ValueTask CompleteActivityAsync(this ActivityExecutionContext context, object? result = default) - { - var outcomes = result as Outcomes; - - // If the activity is executing in the background, simply capture the result and return. - if (context.GetIsBackgroundExecution()) - { - if (outcomes != null) - context.SetBackgroundOutcomes(outcomes.Names); - else - context.SetBackgroundCompletion(); - return; - } - - // If the activity is not running, do nothing. - if (context.Status != ActivityStatus.Running) - return; - - // Update all child contexts. - var childContexts = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => x.ParentActivityExecutionContext == context).ToList(); - - foreach (var childContext in childContexts) - await childContext.CancelActivityAsync(); - - // Mark the activity as complete. - context.TransitionTo(ActivityStatus.Completed); - - // Record the outcomes, if any. - if (outcomes != null) - context.JournalData["Outcomes"] = outcomes.Names; - - // Record the output, if any. - var activity = context.Activity; - var expressionExecutionContext = context.ExpressionExecutionContext; - var activityDescriptor = context.ActivityDescriptor; - var outputDescriptors = activityDescriptor.Outputs; - var outputs = outputDescriptors.ToDictionary(x => x.Name, x => activity.GetOutput(expressionExecutionContext, x.Name)!); - var serializer = context.GetRequiredService(); - - foreach (var outputDescriptor in outputDescriptors) - { - if (outputDescriptor.IsSerializable == false) - continue; - - var outputName = outputDescriptor.Name; - var outputValue = outputs[outputName]; - - if (outputValue == null!) - continue; - - var serializedOutputValue = await serializer.SerializeAsync(outputValue); - context.JournalData[outputName] = serializedOutputValue; - } - - // Add an execution log entry. - context.AddExecutionLogEntry("Completed", payload: context.JournalData); - - // Send a signal. - await context.SendSignalAsync(new ActivityCompleted(result)); - - // Clear bookmarks. - context.ClearBookmarks(); - context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityInstanceId == context.Id); - - // Remove completion callbacks. - context.ClearCompletionCallbacks(); - - // Remove all associated variables, unless this is the root context - in which case we want to keep the variables since we're not deleting that one. - if (context.ParentActivityExecutionContext != null) - { - var variablePersistenceManager = context.GetRequiredService(); - await variablePersistenceManager.DeleteVariablesAsync(context); - } - - // Update the completed at timestamp. - context.CompletedAt = context.WorkflowExecutionContext.SystemClock.UtcNow; - } - - /// - /// Complete the current activity. This should only be called by activities that explicitly suppress automatic-completion. - /// + [RequiresUnreferencedCode("The activity may be serialized and executed in a different context.")] public static async ValueTask ScheduleOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) { var cancellationToken = context.CancellationToken; @@ -525,22 +437,7 @@ public static class ActivityExecutionContextExtensions // Send a signal. await context.SendSignalAsync(new ScheduleActivityOutcomes(outcomes)); } - - /// - /// Complete the current activity with the specified outcome. - /// - public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityCompletedContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes)); - - /// - /// Complete the current activity with the specified outcome. - /// - public static ValueTask CompleteActivityWithOutcomesAsync(this ActivityExecutionContext context, params string[] outcomes) => context.CompleteActivityAsync(new Outcomes(outcomes)); - - /// - /// Complete the current composite activity with the specified outcome. - /// - public static async ValueTask CompleteCompositeAsync(this ActivityExecutionContext context, params string[] outcomes) => await context.SendSignalAsync(new CompleteCompositeSignal(new Outcomes(outcomes))); - + /// /// Cancel the activity. For blocking activities, it means their bookmarks will be removed. For job activities, the background work will be cancelled. /// diff --git a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs index aef490975..422e4f2c6 100644 --- a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs +++ b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkArgs.cs @@ -16,14 +16,14 @@ public class CreateBookmarkArgs /// An optional name to associate with the bookmark. public string? BookmarkName { get; set; } - /// Whether or not the bookmark should be automatically burned when triggered. + /// Whether the bookmark should be automatically burned when triggered. public bool AutoBurn { get; set; } = true; - /// Whether or not the activity instance ID should be included in the bookmark payload. + /// Whether the activity instance ID should be included in the bookmark payload. public bool IncludeActivityInstanceId { get; set; } /// - /// Whether or not the activity being resumed should be automatically completed if CallBack is not specified. + /// Whether the activity being resumed should be automatically completed if CallBack is not specified. /// public bool AutoComplete { get; set; } = true; diff --git a/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs b/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs index 997c70296..7c14eb238 100644 --- a/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs +++ b/src/modules/Elsa.Workflows.Core/Services/BookmarkHasher.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Elsa.Expressions.Contracts; using Elsa.Workflows.Contracts; @@ -30,6 +31,7 @@ public class BookmarkHasher : IBookmarkHasher } /// + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(Object, Type, JsonSerializerOptions)")] public string Hash(string activityTypeName, object? payload, string? activityInstanceId = default) { var json = payload != null ? Serialize(payload) : null; @@ -47,5 +49,12 @@ public class BookmarkHasher : IBookmarkHasher return hash; } - private string Serialize(object payload) => JsonSerializer.Serialize(payload, payload.GetType(), _settings); + [RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize(Object, Type, JsonSerializerOptions)")] + private string Serialize(object payload) + { + if(payload is string s) + return s; + + return JsonSerializer.Serialize(payload, payload.GetType(), _settings); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs index 93a9795d7..05d9754c8 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowRuntime.cs @@ -276,10 +276,10 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime /// public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, TriggerWorkflowsOptions? options) { - var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options.ActivityInstanceId); - var correlationId = options.CorrelationId; - var workflowInstanceId = options.WorkflowInstanceId; - var activityInstanceId = options.ActivityInstanceId; + var hash = _hasher.Hash(activityTypeName, bookmarkPayload, options?.ActivityInstanceId); + var correlationId = options?.CorrelationId; + var workflowInstanceId = options?.WorkflowInstanceId; + var activityInstanceId = options?.ActivityInstanceId; var filter = new BookmarkFilter { Hash = hash, @@ -287,15 +287,15 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime WorkflowInstanceId = workflowInstanceId, ActivityInstanceId = activityInstanceId }; - var bookmarks = await _bookmarkStore.FindManyAsync(filter, options.CancellationTokens.SystemCancellationToken); + var bookmarks = await _bookmarkStore.FindManyAsync(filter, options?.CancellationTokens.SystemCancellationToken ?? default); return await ResumeWorkflowsAsync( bookmarks, new ResumeWorkflowRuntimeParams { CorrelationId = correlationId, - Input = options.Input, - CancellationTokens = options.CancellationTokens + Input = options?.Input, + CancellationTokens = options?.CancellationTokens ?? default }); }