From 1bf9b11ffc98afd622b091a166b4d30dd09b3eb3 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Tue, 5 Aug 2025 13:50:35 +0200 Subject: [PATCH] Improving behaviour of Run Asynchronously property for the TaskActivityAttribute --- .../Abstractions/Activity.cs | 2 +- .../Extensions/ActivityPropertyExtensions.cs | 9 +++-- .../Services/ActivityFactory.cs | 34 ++++++++++++++++++- .../BackgroundActivityInvokerMiddleware.cs | 24 ++++++++++++- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs index 0ef11e6d8..1548daa0c 100644 --- a/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Abstractions/Activity.cs @@ -68,7 +68,7 @@ public abstract class Activity : IActivity, ISignalHandler /// will execute synchronously, while activities of the kind will execute asynchronously. /// [JsonIgnore] - public bool RunAsynchronously + public bool? RunAsynchronously { get => this.GetRunAsynchronously(); set => this.SetRunAsynchronously(value); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs index ff9bee7c5..f82b56787 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityPropertyExtensions.cs @@ -17,7 +17,7 @@ public static class ActivityPropertyExtensions /// Gets a flag indicating whether this activity can be used for starting a workflow. /// Usually used for triggers, but also used to disambiguate between two or more starting activities and no starting activity was specified. /// - public static bool GetCanStartWorkflow(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CanStartWorkflowPropertyName, () => false); + public static bool GetCanStartWorkflow(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(CanStartWorkflowPropertyName, () => false); /// /// Sets a flag indicating whether this activity can be used for starting a workflow. @@ -29,14 +29,17 @@ public static class ActivityPropertyExtensions /// By default, activities with an of , or /// will execute synchronously, while activities of the kind will execute asynchronously. /// - public static bool GetRunAsynchronously(this IActivity activity) => activity.CustomProperties.GetValueOrDefault(RunAsynchronouslyPropertyName, () => false); + public static bool? GetRunAsynchronously(this IActivity activity) + { + return activity.CustomProperties.GetValueOrDefault(RunAsynchronouslyPropertyName, defaultValueFactory: () => null); + } /// /// Sets a flag indicating if this activity should execute synchronously or asynchronously. /// By default, activities with an of , or /// will execute synchronously, while activities of the kind will execute asynchronously. /// - public static void SetRunAsynchronously(this IActivity activity, bool value) => activity.CustomProperties[RunAsynchronouslyPropertyName[0]] = value; + public static void SetRunAsynchronously(this IActivity activity, bool? value) => activity.CustomProperties[RunAsynchronouslyPropertyName[0]] = value; /// /// Gets the source file and line number where this activity was instantiated, if any. diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs index 081550a0d..6d5169efb 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityFactory.cs @@ -2,6 +2,7 @@ using System.Text.Json.Nodes; using Elsa.Expressions.Helpers; using Elsa.Extensions; +using Elsa.Workflows.Attributes; using Elsa.Workflows.Memory; using Elsa.Workflows.Models; using Humanizer; @@ -34,7 +35,18 @@ public class ActivityFactory : IActivityFactory // 7) Pull out your boolean flags from the cleaned element var canStartWorkflow = GetBoolean(cleanedElement, "canStartWorkflow"); - var runAsynchronously = GetBoolean(cleanedElement, "runAsynchronously"); + var runAsynchronously = GetNullableBoolean(cleanedElement, "runAsynchronously"); + if (runAsynchronously is null) + { + if (context.ActivityDescriptor.Attributes.OfType().FirstOrDefault() is { } taskActivityAttribute) + { + runAsynchronously = taskActivityAttribute.RunAsynchronously; + } + else + { + runAsynchronously = false; + } + } // 8) If composite, setup if (activity is IComposite composite) @@ -197,4 +209,24 @@ public class ActivityFactory : IActivityFactory return false; } + + private static bool? GetNullableBoolean(JsonElement element, string propertyName) + { + var propertyNames = new[] { propertyName.Camelize(), propertyName.Pascalize() }; + + foreach (var name in propertyNames) + { + if (element.TryGetProperty("customProperties", out var customPropertyElement)) + { + if (customPropertyElement.TryGetProperty(name, out var canStartWorkflowElement)) + return (bool?)canStartWorkflowElement.GetValue(); + } + + if (element.TryGetProperty(propertyName.Camelize(), out var property) + && (bool?)property.GetValue() is { } propValue) + return propValue; + } + + return null; + } } diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs index e744870a3..30277beb9 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Elsa.Extensions; using Elsa.Mediator.Contracts; +using Elsa.Workflows.Attributes; using Elsa.Workflows.CommitStates; using Elsa.Workflows.Middleware.Activities; using Elsa.Workflows.Models; @@ -103,7 +104,28 @@ public class BackgroundActivityInvokerMiddleware( return !GetIsBackgroundExecution(context) && context.WorkflowExecutionContext.ExecuteDelegate == null - && (kind is ActivityKind.Job || (kind == ActivityKind.Task && activity.GetRunAsynchronously())); + && (kind is ActivityKind.Job || GetTaskRunAsynchronously(context)); + } + + private static bool GetTaskRunAsynchronously(ActivityExecutionContext context) + { + var activity = context.Activity; + var activityDescriptor = context.ActivityDescriptor; + var kind = activityDescriptor.Kind; + + if (kind is not ActivityKind.Task) + return false; + + var runAsynchronously = activity.GetRunAsynchronously(); + + if (runAsynchronously is null) + { + var taskActivityAttribute = activityDescriptor.Attributes.OfType().FirstOrDefault(); + + return taskActivityAttribute is { RunAsynchronously: true }; + } + + return (bool)runAsynchronously; } private static bool GetIsBackgroundExecution(ActivityExecutionContext context) => context.TransientProperties.ContainsKey(BackgroundActivityExecutionContextExtensions.IsBackgroundExecution);