diff --git a/Elsa.sln.DotSettings b/Elsa.sln.DotSettings index 21965872e..bea4e96fd 100644 --- a/Elsa.sln.DotSettings +++ b/Elsa.sln.DotSettings @@ -1,4 +1,9 @@  + 300 + True + True + True + True True True True diff --git a/src/bundles/Elsa.WorkflowServer.Web/Program.cs b/src/bundles/Elsa.WorkflowServer.Web/Program.cs index f36d9d744..230aa5119 100644 --- a/src/bundles/Elsa.WorkflowServer.Web/Program.cs +++ b/src/bundles/Elsa.WorkflowServer.Web/Program.cs @@ -60,6 +60,7 @@ services .AddActivity() .AddActivity() .AddActivity() + .AddActivity() .AddActivity() .AddActivity() .AddActivity() diff --git a/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-join.tsx b/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-join.tsx new file mode 100644 index 000000000..c42352b63 --- /dev/null +++ b/src/designer/elsa-workflows-designer/src/components/icons/activities/flow-join.tsx @@ -0,0 +1,13 @@ +import {FunctionalComponent, h} from '@stencil/core'; +import {ActivityIconSettings, getActivityIconCssClass} from "./models"; + +export const FlowJoinIcon: FunctionalComponent = (settings) => ( + + + + + + + + +); diff --git a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx index d913aa172..e6bda67ee 100644 --- a/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx +++ b/src/designer/elsa-workflows-designer/src/services/activity-icon-registry.tsx @@ -15,6 +15,7 @@ import { WriteLineIcon } from "../components/icons/activities"; import {WriteHttpResponseIcon} from "../components/icons/activities/write-http-response"; +import {FlowJoinIcon} from "../components/icons/activities/flow-join"; export type ActivityType = string; export type ActivityIcon = (ActivityIconSettings?) => any; @@ -37,6 +38,7 @@ export class ActivityIconRegistry { this.add('Elsa.FlowDecision', settings => ); this.add('Elsa.Event', settings => ); this.add('Elsa.RunJavaScript', settings => ); + this.add('Elsa.FlowJoin', settings => ); } public add(activityType: ActivityType, icon: ActivityIcon) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs new file mode 100644 index 000000000..3ee17c2c7 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs @@ -0,0 +1,53 @@ +using Elsa.Common.Extensions; +using Elsa.Workflows.Core.Activities.Flowchart.Contracts; +using Elsa.Workflows.Core.Activities.Flowchart.Extensions; +using Elsa.Workflows.Core.Activities.Flowchart.Models; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Models; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; + +[Activity("Elsa", "Flow", "Merge multiple branches into a single branch of execution.")] +public class FlowJoin : ActivityBase, IJoinNode +{ + [Input] public Input Mode { get; set; } = new(JoinMode.WaitAll); + + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var flowchartExecutionContext = context.ParentActivityExecutionContext!; + var flowchart = (Flowchart)flowchartExecutionContext.Activity; + var inboundActivities = flowchart.Connections.LeftInboundActivities(this).ToList(); + var flowScope = flowchartExecutionContext.GetProperty(Flowchart.ScopeProperty)!; + var executionCount = flowScope.GetExecutionCount(this); + var mode = context.Get(Mode); + + switch (mode) + { + case JoinMode.WaitAll: + // If all left-inbound activities have executed, complete & continue. + var haveAllInboundActivitiesExecuted = inboundActivities.All(x => flowScope.GetExecutionCount(x) > executionCount); + + if (haveAllInboundActivitiesExecuted) + await context.CompleteActivityAsync(); + break; + case JoinMode.WaitAny: + // Only complete if we haven't already executed. + var alreadyExecuted = inboundActivities.Max(x => flowScope.GetExecutionCount(x)) == executionCount; + + if (!alreadyExecuted) + { + await context.CompleteActivityAsync(); + ClearBookmarks(flowchart, context); + } + break; + } + } + + private void ClearBookmarks(Flowchart flowchart, ActivityExecutionContext context) + { + // Clear any bookmarks created between this join and its most recent fork. + var connections = flowchart.Connections; + var inboundActivities = connections.LeftAncestorActivities(this).Select(x => x.Id).ToList(); + context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => inboundActivities.Contains(x.ActivityId)); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 72681545d..4c5b61213 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -1,3 +1,5 @@ +using Elsa.Workflows.Core.Activities.Flowchart.Contracts; +using Elsa.Workflows.Core.Activities.Flowchart.Extensions; using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; @@ -9,6 +11,8 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; [Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")] public class Flowchart : Container { + internal const string ScopeProperty = "Scope"; + public Flowchart() { OnSignalReceived(OnDescendantCompletedAsync); @@ -32,33 +36,72 @@ public class Flowchart : Container { await ScheduleChildrenAsync(signal, context); } - + private async Task ScheduleChildrenAsync(ActivityCompleted signal, SignalContext context) { - var activityExecutionContext = context.ReceiverActivityExecutionContext; - var parent = context.SenderActivityExecutionContext.Activity; + var flowchartActivityExecutionContext = context.ReceiverActivityExecutionContext; + var completedActivity = context.SenderActivityExecutionContext.Activity; - if (parent == null!) + if (completedActivity == null!) return; // Ignore completed activities that are not immediate children. - var isDirectChild = Activities.Contains(parent); - + var isDirectChild = Activities.Contains(completedActivity); + if (!isDirectChild) return; // If a specific outcome was provided by the completed activity, use it to find the connection to the next activity. - Func outboundConnectionsQuery = signal.Result is Outcome outcome - ? connection => connection.Source == parent && connection.SourcePort == outcome.Name - : connection => connection.Source == parent; + Func outboundConnectionsQuery = signal.Result is Outcome outcome + ? connection => connection.Source == completedActivity && connection.SourcePort == outcome.Name + : connection => connection.Source == completedActivity; var outboundConnections = Connections.Where(outboundConnectionsQuery).ToList(); var children = outboundConnections.Select(x => x.Target).ToList(); + var scope = flowchartActivityExecutionContext.GetProperty(ScopeProperty, () => new FlowScope()); + + scope.RegisterActivityExecution(completedActivity); if (children.Any()) - activityExecutionContext.ScheduleActivities(children); - else - await activityExecutionContext.CompleteActivityAsync(); + { + scope.AddActivities(children); + + // Schedule each child, but only if all of its left inbound activities have already executed. + foreach (var activity in children) + { + var inboundActivities = Connections.LeftInboundActivities(activity).ToList(); + + // If the completed activity is not part of the left inbound path, always allow its children to be scheduled. + if (!inboundActivities.Contains(completedActivity)) + { + flowchartActivityExecutionContext.ScheduleActivity(activity); + continue; + } + + // If the activity is anything but a join activity, only schedule it if all of its left-inbound activities have executed, effectively implementing a "wait all" join. + if (activity is not IJoinNode) + { + var executionCount = scope.GetExecutionCount(activity); + var haveInboundActivitiesExecuted = inboundActivities.All(x => scope.GetExecutionCount(x) > executionCount); + + if (haveInboundActivitiesExecuted) + flowchartActivityExecutionContext.ScheduleActivity(activity); + } + else + { + flowchartActivityExecutionContext.ScheduleActivity(activity); + } + } + } + + if (!children.Any()) + { + // If there are no more pending activities in any of the scopes, mark this activity as completed. + var hasPendingChildren = scope.HasPendingActivities(); + + if (!hasPendingChildren) + await flowchartActivityExecutionContext.CompleteActivityAsync(); + } context.StopPropagation(); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Contracts/IJoinNode.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Contracts/IJoinNode.cs new file mode 100644 index 000000000..b962eebe9 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Contracts/IJoinNode.cs @@ -0,0 +1,10 @@ +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Contracts; + +/// +/// Gives implementing activities a chance to customize certain flowchart execution behaviors. +/// +public interface IJoinNode : IActivity +{ +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs index 9a408c084..41376aebf 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/ConnectionsExtensions.cs @@ -5,18 +5,75 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Extensions; public static class ConnectionsExtensions { - public static IEnumerable Descendants(this ICollection allConnections, IActivity parent) + public static IEnumerable Descendants(this ICollection connections, IActivity parent) { - var children = allConnections.Where(x => parent == x.Source).ToList(); + var visitedActivities = new HashSet(); + return connections.Descendants(parent, visitedActivities); + } + + public static IEnumerable Ancestors(this ICollection connections, IActivity activity) + { + var visitedActivities = new HashSet(); + return connections.Ancestors(activity, visitedActivities); + } + + public static IEnumerable InboundConnections(this ICollection connections, IActivity activity) => connections.Where(x => x.Target == activity).ToList(); + + public static IEnumerable LeftInboundConnections(this ICollection connections, IActivity activity) + { + // We only take "left" inbound connections, which means we exclude descendent connections looping back. + var descendantConnections = connections.Descendants(activity).ToList(); + var filteredConnections = connections.InboundConnections(activity).Except(descendantConnections).ToList(); + + return filteredConnections; + } + + public static IEnumerable LeftAncestorConnections(this ICollection connections, IActivity activity) + { + // We only take "left" inbound connections, which means we exclude descendent connections looping back. + var descendantConnections = connections.Descendants(activity).ToList(); + var filteredConnections = connections.Ancestors(activity).Except(descendantConnections).ToList(); + + return filteredConnections; + } + + public static IEnumerable InboundActivities(this ICollection connections, IActivity activity) => connections.InboundConnections(activity).Select(x => x.Source); + public static IEnumerable LeftInboundActivities(this ICollection connections, IActivity activity) => connections.LeftInboundConnections(activity).Select(x => x.Source); + public static IEnumerable LeftAncestorActivities(this ICollection connections, IActivity activity) => connections.LeftAncestorConnections(activity).Select(x => x.Source); + + private static IEnumerable Descendants(this ICollection connections, IActivity parent, ISet visitedActivities) + { + var children = connections.Where(x => parent == x.Source && !visitedActivities.Contains(x.Target)).ToList(); foreach (var child in children) { + visitedActivities.Add(child.Target); yield return child; - var descendants = allConnections.Descendants(child.Target).ToList(); + var descendants = connections.Descendants(child.Target, visitedActivities).ToList(); foreach (var descendant in descendants) + { yield return descendant; + } + } + } + + private static IEnumerable Ancestors(this ICollection connections, IActivity activity, ISet visitedActivities) + { + var parents = connections.Where(x => activity == x.Target && !visitedActivities.Contains(x.Source)).ToList(); + + foreach (var parent in parents) + { + visitedActivities.Add(parent.Source); + yield return parent; + + var ancestors = connections.Ancestors(parent.Source, visitedActivities).ToList(); + + foreach (var ancestor in ancestors) + { + yield return ancestor; + } } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ActivityFlowState.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ActivityFlowState.cs new file mode 100644 index 000000000..eb601fa1d --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/ActivityFlowState.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Models; + +[DebuggerDisplay("ActivityId = {ActivityId}, ExecutionCount = {ExecutionCount}")] +public class ActivityFlowState +{ + [JsonConstructor] + public ActivityFlowState() + { + } + + public ActivityFlowState(string activityId, long executionCount = 0) + { + ActivityId = activityId; + ExecutionCount = executionCount; + } + + public string ActivityId { get; set; } = default!; + public long ExecutionCount { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs new file mode 100644 index 000000000..e958bc7a3 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowScope.cs @@ -0,0 +1,75 @@ +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Activities.Flowchart.Models; + +public class FlowScope +{ + [JsonConstructor] + public FlowScope() + { + } + + public FlowScope(string ownerActivityId) + { + OwnerActivityId = ownerActivityId; + Activities.Add(ownerActivityId, new ActivityFlowState(ownerActivityId, 1)); + } + + /// + /// The activity from which the scope was created. + /// + public string OwnerActivityId { get; set; } = default!; + + /// + /// A list of scheduled activity IDs and a flag whether they executed or not. + /// + public IDictionary Activities { get; set; } = + new Dictionary(); + + public void AddActivities(IEnumerable activities, long executionCount = 0) + { + foreach (var activity in activities) + AddActivity(activity, executionCount); + } + + public void AddActivity(IActivity activity, long executionCount = 0) => EnsureActivity(activity, executionCount); + + public ActivityFlowState EnsureActivity(IActivity activity, long executionCount = 0) + { + if (Activities.ContainsKey(activity.Id)) + return Activities[activity.Id]; + + var state = new ActivityFlowState(activity.Id, executionCount); + Activities.Add(activity.Id, state); + return state; + + } + + public void RegisterActivityExecution(IActivity activity) + { + var state = Activities.TryGetValue(activity.Id, out var s) ? s : default; + + if (state == null) + { + state = new ActivityFlowState(activity.Id); + Activities[activity.Id] = state; + } + + state.ExecutionCount++; + } + + /// + /// Return a list excluding any activities that already executed. + /// + public IEnumerable ExcludeExecutedActivities(IEnumerable activities) => + activities.Where(x => !Activities.ContainsKey(x.Id) || Activities[x.Id].ExecutionCount == 0); + + public bool HasPendingActivities() + { + var sample = Activities.Values.First().ExecutionCount; + return Activities.Values.Any(x => x.ExecutionCount != sample); + } + + public long GetExecutionCount(IActivity activity) => EnsureActivity(activity).ExecutionCount; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/JoinMode.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/JoinMode.cs new file mode 100644 index 000000000..8e276740e --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/JoinMode.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; + +public enum JoinMode +{ + WaitAll, + WaitAny +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs index 0ccca9e04..7944b0750 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/ActivityInvoker.cs @@ -21,22 +21,10 @@ public class ActivityInvoker : IActivityInvoker ActivityExecutionContext? owner, IEnumerable? memoryReferences = default) { - var cancellationToken = workflowExecutionContext.CancellationToken; - - // Get a handle to the parent execution context. - var parentActivityExecutionContext = owner; - var parentExpressionExecutionContext = parentActivityExecutionContext?.ExpressionExecutionContext; - // Setup an activity execution context. var workflowMemory = workflowExecutionContext.MemoryRegister; - var workflow = workflowExecutionContext.Workflow; - var transientProperties = workflowExecutionContext.TransientProperties; - var input = workflowExecutionContext.Input; - var applicationProperties = ExpressionExecutionContextExtensions.CreateApplicationPropertiesFrom(workflow, transientProperties, input); - var parentMemory = parentActivityExecutionContext?.ExpressionExecutionContext.Memory ?? workflowMemory; var activityMemory = new MemoryRegister(workflowMemory); - var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, parentMemory, parentExpressionExecutionContext, applicationProperties, cancellationToken); - var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, parentActivityExecutionContext, expressionExecutionContext, activity, cancellationToken); + var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContext(activity, owner); // Declare memory. if (memoryReferences != null) diff --git a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs index 9ee8be0d3..45eba9407 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs @@ -78,7 +78,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer foreach (var entry in state.ActivityOutput) { var activityId = entry.Key; - var node = workflowExecutionContext.FindActivityNodeById(activityId); + var node = workflowExecutionContext.FindNodeById(activityId); var activityType = node.Activity.GetType(); foreach (var outputEntry in entry.Value) @@ -96,7 +96,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer foreach (var completionCallbackEntry in state.CompletionCallbacks) { var owner = workflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == completionCallbackEntry.OwnerId); - var child = workflowExecutionContext.FindActivityNodeById(completionCallbackEntry.ChildId).Activity; + var child = workflowExecutionContext.FindNodeById(completionCallbackEntry.ChildId).Activity; var callbackName = completionCallbackEntry.MethodName; var callbackDelegate = owner.Activity.GetActivityCompletionCallback(callbackName); workflowExecutionContext.AddCompletionCallback(owner, child, callbackDelegate); @@ -133,21 +133,11 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer { ActivityExecutionContext CreateActivityExecutionContext(ActivityExecutionContextState activityExecutionContextState) { - var cancellationToken = workflowExecutionContext.CancellationToken; var activity = workflowExecutionContext.FindActivityById(activityExecutionContextState.ScheduledActivityId); - var workflowMemory = workflowExecutionContext.MemoryRegister; - var workflow = workflowExecutionContext.Workflow; - var expressionInput = workflowExecutionContext.Input; - var transientProperties = workflowExecutionContext.TransientProperties; - var applicationProperties = ExpressionExecutionContextExtensions.CreateApplicationPropertiesFrom(workflow, transientProperties, expressionInput); - var activityMemory = new MemoryRegister(workflowMemory); - var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, activityMemory, null, applicationProperties, cancellationToken); var properties = activityExecutionContextState.Properties; - var activityExecutionContext = new ActivityExecutionContext(workflowExecutionContext, default, expressionExecutionContext, activity, cancellationToken) - { - Id = activityExecutionContextState.Id, - ApplicationProperties = properties - }; + var activityExecutionContext = workflowExecutionContext.CreateActivityExecutionContext(activity); + activityExecutionContext.Id = activityExecutionContextState.Id; + activityExecutionContext.ApplicationProperties = properties; return activityExecutionContext; } diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs index 0555af20c..fe1faccee 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Text.Json.Serialization; using Elsa.Workflows.Core.Behaviors; using Elsa.Workflows.Core.Helpers; @@ -5,6 +6,7 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Models; +[DebuggerDisplay("{Type} - {Id}")] public abstract class ActivityBase : IActivity, ISignalHandler { private readonly ICollection _signalHandlers = new List(); diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs index 8f77b6a75..e02d4f2aa 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs @@ -135,6 +135,18 @@ public class ActivityExecutionContext public void ClearBookmarks() => _bookmarks.Clear(); public T? GetProperty(string key) => ApplicationProperties!.TryGetValue(key, out var value) ? value : default; + + public T GetProperty(string key, Func defaultValue) + { + if (ApplicationProperties.TryGetValue(key, out var value)) + return value!; + + value = defaultValue(); + ApplicationProperties[key] = value!; + + return value!; + } + public void SetProperty(string key, T? value) => ApplicationProperties[key] = value!; public T UpdateProperty(string key, Func updater) where T : notnull diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs index 4373433cc..bbb5a6d12 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityNode.cs @@ -22,27 +22,27 @@ public class ActivityNode foreach (var child in Children) { yield return child; - + var descendants = child.Descendants(); foreach (var descendant in descendants) yield return descendant; } } - + public IEnumerable Ancestors() { foreach (var parent in Parents) { yield return parent; - + var ancestors = parent.Ancestors(); foreach (var ancestor in ancestors) yield return ancestor; } } - + public IEnumerable Siblings() => Parents.SelectMany(parent => parent.Children); public IEnumerable SiblingsAndCousins() => Parents.SelectMany(parent => parent.Descendants()); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs index 52908254b..4230b7f5e 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs @@ -109,9 +109,9 @@ public class WorkflowExecutionContext _completionCallbackEntries.Remove(entry); } - public ActivityNode FindActivityNodeById(string nodeId) => NodeIdLookup[nodeId]; + public ActivityNode FindNodeById(string nodeId) => NodeIdLookup[nodeId]; public ActivityNode FindNodeByActivity(IActivity activity) => NodeActivityLookup[activity]; - public IActivity FindActivityById(string activityId) => FindActivityNodeById(activityId).Activity; + public IActivity FindActivityById(string activityId) => FindNodeById(activityId).Activity; public T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? (T?)value : default(T); public void SetProperty(string key, T value) => Properties[key] = value!; @@ -134,6 +134,16 @@ public class WorkflowExecutionContext SubStatus = subStatus; } + public ActivityExecutionContext CreateActivityExecutionContext(IActivity activity, ActivityExecutionContext? parentContext = default) + { + var parentExpressionExecutionContext = parentContext?.ExpressionExecutionContext; + var applicationProperties = ExpressionExecutionContextExtensions.CreateApplicationPropertiesFrom(Workflow, TransientProperties, Input); + var parentMemory = parentContext?.ExpressionExecutionContext.Memory ?? MemoryRegister; + var expressionExecutionContext = new ExpressionExecutionContext(_serviceProvider, parentMemory, parentExpressionExecutionContext, applicationProperties, CancellationToken); + var activityExecutionContext = new ActivityExecutionContext(this, parentContext, expressionExecutionContext, activity, CancellationToken); + return activityExecutionContext; + } + private WorkflowStatus GetMainStatus(WorkflowSubStatus subStatus) => subStatus switch { diff --git a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs index 33afd1a5d..d1247f842 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/ActivityDescriber.cs @@ -98,7 +98,8 @@ public class ActivityDescriber : IActivityDescriber { var inputAttribute = propertyInfo.GetCustomAttribute(); var descriptionAttribute = propertyInfo.GetCustomAttribute(); - var wrappedPropertyType = propertyInfo.PropertyType.GenericTypeArguments[0]; + var propertyType = propertyInfo.PropertyType; + var wrappedPropertyType = !typeof(Input).IsAssignableFrom(propertyType) ? propertyType : propertyInfo.PropertyType.GenericTypeArguments[0]; yield return new InputDescriptor ( diff --git a/src/modules/Elsa.Workflows.Management/Implementations/PropertyOptionsResolver.cs b/src/modules/Elsa.Workflows.Management/Implementations/PropertyOptionsResolver.cs index 2008b4dd3..675951908 100644 --- a/src/modules/Elsa.Workflows.Management/Implementations/PropertyOptionsResolver.cs +++ b/src/modules/Elsa.Workflows.Management/Implementations/PropertyOptionsResolver.cs @@ -1,5 +1,6 @@ using System.Reflection; using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Models; using Elsa.Workflows.Management.Extensions; using Elsa.Workflows.Management.Models; using Elsa.Workflows.Management.Services; @@ -38,13 +39,14 @@ public class PropertyOptionsResolver : IPropertyOptionsResolver { var isNullable = activityPropertyInfo.PropertyType.IsNullableType(); var propertyType = isNullable ? activityPropertyInfo.PropertyType.GetTypeOfNullable() : activityPropertyInfo.PropertyType; + var wrappedPropertyType = !typeof(Input).IsAssignableFrom(propertyType) ? propertyType : activityPropertyInfo.PropertyType.GenericTypeArguments[0]; items = null; - if (!propertyType.IsEnum) + if (!wrappedPropertyType.IsEnum) return false; - items = propertyType.GetEnumNames().Select(x => new SelectListItem(x.Humanize(LetterCasing.Title), x)).ToList(); + items = wrappedPropertyType.GetEnumNames().Select(x => new SelectListItem(x.Humanize(LetterCasing.Title), x)).ToList(); if (isNullable) items.Insert(0, new SelectListItem("-", ""));