From d18809baa19064d316f9f6007760bfe42d51c92d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Tue, 30 Sep 2025 20:12:48 +0200 Subject: [PATCH 1/4] Bump workflow base version and branch references to `3.5.2`. --- .github/workflows/elsa-server-and-studio.yml | 2 +- .github/workflows/elsa-server.yml | 2 +- .github/workflows/elsa-studio.yml | 2 +- .github/workflows/packages.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/elsa-server-and-studio.yml b/.github/workflows/elsa-server-and-studio.yml index 09873318e..d4a703432 100644 --- a/.github/workflows/elsa-server-and-studio.yml +++ b/.github/workflows/elsa-server-and-studio.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - patch/3.5.1 + - patch/3.5.2 jobs: push_to_registry: diff --git a/.github/workflows/elsa-server.yml b/.github/workflows/elsa-server.yml index 3011af454..9ca17578f 100644 --- a/.github/workflows/elsa-server.yml +++ b/.github/workflows/elsa-server.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - patch/3.5.1 + - patch/3.5.2 jobs: push_to_registry: diff --git a/.github/workflows/elsa-studio.yml b/.github/workflows/elsa-studio.yml index 408631ceb..2b59b07ba 100644 --- a/.github/workflows/elsa-studio.yml +++ b/.github/workflows/elsa-studio.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - patch/3.5.1 + - patch/3.5.2 jobs: push_to_registry: diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index e57c9a589..cb2df493f 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -15,7 +15,7 @@ on: release: types: [ prereleased, published ] env: - base_version: '3.5.1' + base_version: '3.5.2' feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json' nuget_feed_source: 'https://api.nuget.org/v3/index.json' @@ -48,7 +48,7 @@ jobs: run: | if [[ "${{ github.ref }}" == refs/tags/* && "${{ github.event_name }}" == "release" && ("${{ github.event.action }}" == "published" || "${{ github.event.action }}" == "prereleased")]]; then git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* - git branch --remote --contains | grep origin/patch/3.5.1 + git branch --remote --contains | grep origin/patch/3.5.2 else git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* git branch --remote --contains | grep origin/${BRANCH_NAME} From c6974a4e341d5c9a29f64949fdddcc8df083d9e3 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 1 Oct 2025 15:01:22 +0200 Subject: [PATCH 2/4] Improves Flowchart activity robustness (#6938) * Bump workflow base version and branch references to `3.5.2`. * Refactor flowchart tests and `Flowchart` activity for improved readability and consistency, alongside minor code cleanup. * Adds comment for clarity. Adds a comment to explain the continue statement within the flowchart execution logic. This improves code readability and maintainability. * Refactor flowchart test to remove unused cases, update switch behavior, and adjust expected output for improved consistency and clarity. --- .../Flowchart/Activities/Flowchart.cs | 28 +-- .../Scenarios/FlowchartNextActivity/Tests.cs | 187 ++++++++++-------- 2 files changed, 125 insertions(+), 90 deletions(-) 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 03e333b32..37903d923 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -151,7 +151,7 @@ public class Flowchart : Container if (flowchartContext.Activity != this) { - throw new Exception("Target context activity must be this flowchart"); + throw new("Target context activity must be this flowchart"); } // If the completed activity's status is anything but "Completed", do not schedule its outbound activities. @@ -167,16 +167,16 @@ public class Flowchart : Container return; } - // Determine the outcomes from the completed activity + // Determine the outcomes from the completed activity. var outcomes = result is Outcomes o ? o : Outcomes.Default; // Schedule the outbound activities var flowGraph = GetFlowGraph(flowchartContext); var flowScope = GetFlowScope(flowchartContext); var completedActivityExcecutedByBackwardConnection = completedActivityContext.ActivityInput.GetValueOrDefault(BackwardConnectionActivityInput); - bool hasScheduledActivity = await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, completedActivityExcecutedByBackwardConnection); + var hasScheduledActivity = await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, completedActivityExcecutedByBackwardConnection); - // If there are not any outbound connections, complete the flowchart activity if there is no other pending work + // If there are not any outbound connections, complete the flowchart activity if there is no other pending work. if (!hasScheduledActivity) { await CompleteIfNoPendingWorkAsync(flowchartContext); @@ -198,12 +198,12 @@ public class Flowchart : Container /// True if at least one activity was scheduled; otherwise, false. private async ValueTask ScheduleOutboundActivitiesAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity activity, Outcomes outcomes, bool completedActivityExecutedByBackwardConnection = false) { - bool hasScheduledActivity = false; + var hasScheduledActivity = false; // Check if the activity is dangling (i.e., it is not reachable from the flowchart graph) if (flowGraph.IsDanglingActivity(activity)) { - throw new Exception($"Activity {activity.Id} is not reachable from the flowchart graph. Unable to schedule it's outbound activities."); + throw new($"Activity {activity.Id} is not reachable from the flowchart graph. Unable to schedule it's outbound activities."); } // Register the activity as visited unless it was executed due to a backward connection @@ -215,12 +215,16 @@ public class Flowchart : Container // Process each outbound connection from the current activity foreach (var outboundConnection in flowGraph.GetOutboundConnections(activity)) { - bool connectionFollowed = outcomes.Names.Contains(outboundConnection.Source.Port); + var connectionFollowed = outcomes.Names.Contains(outboundConnection.Source.Port); flowScope.RegisterConnectionVisit(outboundConnection, connectionFollowed); + + if(!connectionFollowed) + continue; // Skip if connection was not followed. + var outboundActivity = outboundConnection.Target.Activity; - // Determine scheduling strategy based on connection type - if (flowGraph.IsBackwardConnection(outboundConnection, out bool backwardConnectionIsValid)) + // Determine the scheduling strategy based on connection-type. + if (flowGraph.IsBackwardConnection(outboundConnection, out var backwardConnectionIsValid)) { hasScheduledActivity |= await ScheduleBackwardConnectionActivityAsync(flowGraph, flowchartContext, outboundConnection, outboundActivity, connectionFollowed, backwardConnectionIsValid); } @@ -248,7 +252,7 @@ public class Flowchart : Container if (!backwardConnectionIsValid) { - throw new Exception($"Invalid backward connection: Every path from the source ('{outboundConnection.Source.Activity.Id}') must go through the target ('{outboundConnection.Target.Activity.Id}') when tracing back to the start."); + throw new($"Invalid backward connection: Every path from the source ('{outboundConnection.Source.Activity.Id}') must go through the target ('{outboundConnection.Target.Activity.Id}') when tracing back to the start."); } var scheduleWorkOptions = new ScheduleWorkOptions @@ -395,7 +399,7 @@ public class Flowchart : Container if (activityExecutionContext != null) { - await flowchartContext.ScheduleActivityAsync(activityExecutionContext.Activity, new ScheduleWorkOptions + await flowchartContext.ScheduleActivityAsync(activityExecutionContext.Activity, new() { ExistingActivityExecutionContext = activityExecutionContext, CompletionCallback = OnChildCompletedAsync, @@ -404,7 +408,7 @@ public class Flowchart : Container } else { - await flowchartContext.ScheduleActivityAsync(activity, new ScheduleWorkOptions + await flowchartContext.ScheduleActivityAsync(activity, new() { CompletionCallback = OnChildCompletedAsync, Input = signal.Input diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs index 82eb8c22b..460c2d2a8 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs @@ -32,7 +32,10 @@ public class FlowchartNextActivityTests await _services.PopulateRegistriesAsync(); await _workflowRunner.RunAsync(); var lines = _capturingTextWriter.Lines.ToList(); - Assert.Equal(new[] { "Line 1" }, lines); + Assert.Equal(new[] + { + "Line 1" + }, lines); } [Fact(DisplayName = "Flowchart with backward connections and a dangling activity")] @@ -46,15 +49,14 @@ public class FlowchartNextActivityTests var dangling = new WriteLine("dangling"); var writeLineDecision = new FlowSwitch() { - Cases = { - new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")), - new FlowSwitchCase("LessThanOne", new Expression("JavaScript", "getVariable('LoopCount') < 1")), - }, + Cases = + { + new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")) + }, Mode = new(SwitchMode.MatchAny) }; var a = new WriteLine("A"); var b = new WriteLine("B"); - var c = new WriteLine("C"); var incrementLoop = new SetVariable() { Variable = loopVariable, @@ -62,56 +64,54 @@ public class FlowchartNextActivityTests }; var loopbackDecision = new FlowSwitch() { - Cases = { + Cases = + { new FlowSwitchCase("EqualOne", new Expression("JavaScript", "getVariable('LoopCount') == 1")), new FlowSwitchCase("LessThanFour", new Expression("JavaScript", "getVariable('LoopCount') < 4")), - }, - Mode = new(SwitchMode.MatchFirst) + new FlowSwitchCase("EqualThree", new Expression("JavaScript", "getVariable('LoopCount') == 3")), + }, + Mode = new(SwitchMode.MatchAny) }; var d = new WriteLine("D"); var e = new WriteLine("E"); var f = new WriteLine("F"); var end = new End(); - workflowBuilder.Root = new Flowchart { Variables = - { - loopVariable - }, + { + loopVariable + }, Activities = - { - start, - dangling, - writeLineDecision, - a, - b, - c, - incrementLoop, - loopbackDecision, - d, - e, - f, - end - }, + { + start, + dangling, + writeLineDecision, + a, + b, + incrementLoop, + loopbackDecision, + d, + e, + f, + end + }, Connections = { new(start, writeLineDecision), new(dangling, writeLineDecision), new(new Endpoint(writeLineDecision, "LessThanThree"), new Endpoint(a)), new(new Endpoint(writeLineDecision, "LessThanThree"), new Endpoint(b)), - new(new Endpoint(writeLineDecision, "LessThanOne"), new Endpoint(c)), new(new Endpoint(writeLineDecision, "Default"), new Endpoint(incrementLoop)), new(a, incrementLoop), new(b, incrementLoop), - new(c, incrementLoop), new(incrementLoop, loopbackDecision), new(new Endpoint(loopbackDecision, "EqualOne"), new Endpoint(d)), new(d, incrementLoop), new(new Endpoint(loopbackDecision, "LessThanFour"), new Endpoint(e)), new(e, writeLineDecision), - new(new Endpoint(loopbackDecision, "Default"), new Endpoint(f)), + new(new Endpoint(loopbackDecision, "EqualThree"), new Endpoint(f)), new(f, end), } }; @@ -121,7 +121,10 @@ public class FlowchartNextActivityTests var result = await _workflowRunner.RunAsync(workflow); var lines = _capturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); - Assert.Equal(new[] { "A", "B", "C", "D", "E", "A", "B", "E", "F" }, lines); + Assert.Equal(new[] + { + "A", "B", "D", "E", "A", "B", "E", "E", "F" + }, lines); } [Fact(DisplayName = "Flowchart with an invalid backward connection")] @@ -129,24 +132,42 @@ public class FlowchartNextActivityTests { var workflow = new TestWorkflow(workflowBuilder => { - var start = new Start() { Id = "Start" }; - var a = new WriteLine("A") { Id = "WriteLineA" }; - var b = new WriteLine("B") { Id = "WriteLineB" }; - var c = new WriteLine("C") { Id = "WriteLineC" }; - var d = new WriteLine("D") { Id = "WriteLineD" }; - var e = new WriteLine("E") { Id = "WriteLineE" }; + var start = new Start() + { + Id = "Start" + }; + var a = new WriteLine("A") + { + Id = "WriteLineA" + }; + var b = new WriteLine("B") + { + Id = "WriteLineB" + }; + var c = new WriteLine("C") + { + Id = "WriteLineC" + }; + var d = new WriteLine("D") + { + Id = "WriteLineD" + }; + var e = new WriteLine("E") + { + Id = "WriteLineE" + }; workflowBuilder.Root = new Flowchart { Activities = - { - start, - a, - b, - c, - d, - e, - }, + { + start, + a, + b, + c, + d, + e, + }, Connections = { new(start, a), @@ -166,7 +187,10 @@ public class FlowchartNextActivityTests Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus); Assert.Equal(1, result.WorkflowState.Incidents.Count()); Assert.Equal("Invalid backward connection: Every path from the source ('WriteLineE') must go through the target ('WriteLineC') when tracing back to the start.", result.WorkflowState.Incidents.First().Message); - Assert.Equal(new[] { "A", "B", "C", "D", "E" }, lines); + Assert.Equal(new[] + { + "A", "B", "C", "D", "E" + }, lines); } [Theory(DisplayName = "Flowchart with a Join activity executed multiple times")] @@ -194,7 +218,8 @@ public class FlowchartNextActivityTests }; var loopbackDecision = new FlowSwitch() { - Cases = { + Cases = + { new FlowSwitchCase("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")), }, Mode = new(SwitchMode.MatchFirst) @@ -205,21 +230,21 @@ public class FlowchartNextActivityTests workflowBuilder.Root = new Flowchart { Variables = - { - loopVariable - }, + { + loopVariable + }, Activities = - { - start, - a, - b, - c, - d, - join, - incrementLoop, - loopbackDecision, - end - }, + { + start, + a, + b, + c, + d, + join, + incrementLoop, + loopbackDecision, + end + }, Connections = { new(start, a), @@ -230,7 +255,7 @@ public class FlowchartNextActivityTests new(c, join), new(d, join), new(join, incrementLoop), - new(incrementLoop,loopbackDecision), + new(incrementLoop, loopbackDecision), new(new Endpoint(loopbackDecision, "LessThanThree"), new Endpoint(a)), new(new Endpoint(loopbackDecision, "Default"), new Endpoint(end)), } @@ -241,8 +266,10 @@ public class FlowchartNextActivityTests var result = await _workflowRunner.RunAsync(workflow); var lines = _capturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); - Assert.Equal(new[] { "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D"}, lines); - + Assert.Equal(new[] + { + "A", "B", "C", "D", "A", "B", "C", "D", "A", "B", "C", "D" + }, lines); } [Theory(DisplayName = "Flowchart with a Join activity executed multiple times, bug 6479")] @@ -257,7 +284,8 @@ public class FlowchartNextActivityTests var start = new Start(); var loopbackSwitch = new FlowSwitch() { - Cases = { + Cases = + { new FlowSwitchCase("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")), }, Mode = new(SwitchMode.MatchFirst) @@ -279,19 +307,19 @@ public class FlowchartNextActivityTests workflowBuilder.Root = new Flowchart { Variables = - { - loopVariable - }, + { + loopVariable + }, Activities = - { - start, - loopbackSwitch, - a, - incrementLoop, - join, - b, - end - }, + { + start, + loopbackSwitch, + a, + incrementLoop, + join, + b, + end + }, Connections = { new(start, loopbackSwitch), @@ -310,6 +338,9 @@ public class FlowchartNextActivityTests var result = await _workflowRunner.RunAsync(workflow); var lines = _capturingTextWriter.Lines.ToList(); Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); - Assert.Equal(new[] { "A", "A", "A", "B" }, lines); + Assert.Equal(new[] + { + "A", "A", "A", "B" + }, lines); } } \ No newline at end of file From 32d6f903c5c297805047c6710e92002356336572 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Oct 2025 18:44:38 +0100 Subject: [PATCH 3/4] Resolves Workflow Execution Bodiless Requests Issue. (#6944) * Resolves workflow execution bodiless requests issue. Removed content length check when processing JSON requests, relying solely on content type validation. Added a null-check for deserialized requests to handle cases where the request body is missing or invalid, providing a specific error message. * Refactor JSON request handling in PostEndpoint Updated to get unit tests to pass. --- .../Execute/PostEndpoint.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/PostEndpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/PostEndpoint.cs index 6f787ae4d..4081bc4dc 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/PostEndpoint.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/PostEndpoint.cs @@ -30,19 +30,24 @@ internal class PostEndpoint( { PostRequest? request = null; - if (HttpContext.Request.ContentLength > 0 && (HttpContext.Request.ContentType?.Contains("application/json") ?? true)) + if (HttpContext.Request.ContentType?.Contains("application/json") ?? false) { - try + using var reader = new StreamReader(HttpContext.Request.Body); + var body = await reader.ReadToEndAsync(); + + if (!string.IsNullOrWhiteSpace(body)) { - request = await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, - new JsonSerializerOptions + try { - PropertyNameCaseInsensitive = true - }, cancellationToken: cancellationToken); - } - catch - { - AddError("Invalid request body."); + request = JsonSerializer.Deserialize(body, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + } + catch + { + AddError("Invalid request body."); + } } } From 5daff43cac9697900fc110a67ae7917f1b83dedf Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 3 Oct 2025 23:40:23 +0100 Subject: [PATCH 4/4] Resolve FlowChart merge errors. --- .../Flowchart/Activities/Flowchart.cs | 339 +----------------- 1 file changed, 1 insertion(+), 338 deletions(-) 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 412e23c38..6c72282e7 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -30,7 +30,7 @@ public partial class Flowchart : Container /// /// The activity to execute when the flowchart starts. /// - [Port] [Browsable(false)] public IActivity? Start { get; set; } + [Port][Browsable(false)] public IActivity? Start { get; set; } /// /// A list of connections between activities. @@ -52,343 +52,6 @@ public partial class Flowchart : Container await context.ScheduleActivityAsync(startActivity, OnChildCompletedAsync); } - private IActivity? GetStartActivity(ActivityExecutionContext context) - { - // If there's a trigger that triggered this workflow, use that. - var triggerActivityId = context.WorkflowExecutionContext.TriggerActivityId; - var triggerActivity = triggerActivityId != null ? Activities.FirstOrDefault(x => x.Id == triggerActivityId) : null; - - if (triggerActivity != null) - return triggerActivity; - - // If an explicit Start activity was provided, use that. - if (Start != null) - return Start; - - // If there is a Start activity on the flowchart, use that. - var startActivity = Activities.FirstOrDefault(x => x is Start); - - if (startActivity != null) - return startActivity; - - // If there's an activity marked as "Can Start Workflow", use that. - var canStartWorkflowActivity = Activities.FirstOrDefault(x => x.GetCanStartWorkflow()); - - if (canStartWorkflowActivity != null) - return canStartWorkflowActivity; - - // If there is a single activity that has no inbound connections, use that. - var root = GetRootActivity(); - - if (root != null) - return root; - - // If no start activity found, return the first activity. - return Activities.FirstOrDefault(); - } - - /// - /// Checks if there is any pending work for the flowchart. - /// - private bool HasPendingWork(ActivityExecutionContext context) - { - var workflowExecutionContext = context.WorkflowExecutionContext; - var activityIds = Activities.Select(x => x.Id).ToList(); - var children = context.Children; - var hasRunningActivityInstances = children.Where(x => activityIds.Contains(x.Activity.Id)).Any(x => x.Status == ActivityStatus.Running); - - var hasPendingWork = workflowExecutionContext.Scheduler.List().Any(workItem => - { - var ownerInstanceId = workItem.Owner?.Id; - - if (ownerInstanceId == null) - return false; - - if (ownerInstanceId == context.Id) - return true; - - var ownerContext = context.WorkflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == ownerInstanceId); - var ancestors = ownerContext.GetAncestors().ToList(); - - return ancestors.Any(x => x == context); - }); - - return hasRunningActivityInstances || hasPendingWork; - } - - private IActivity? GetRootActivity() - { - // Get the first activity that has no inbound connections. - var query = - from activity in Activities - let inboundConnections = Connections.Any(x => x.Target.Activity == activity) - where !inboundConnections - select activity; - - var rootActivity = query.FirstOrDefault(); - return rootActivity; - } - - private FlowGraph GetFlowGraph(ActivityExecutionContext context) - { - // Store in TransientProperties so FlowChart is not persisted in WorkflowState - return context.TransientProperties.GetOrAdd(GraphTransientProperty, () => new FlowGraph(Connections, GetStartActivity(context))); - } - - private FlowScope GetFlowScope(ActivityExecutionContext context) - { - return context.GetProperty(ScopeProperty, () => new FlowScope()); - } - - private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context) - { - var flowchartContext = context.TargetContext; - var completedActivityContext = context.ChildContext; - var completedActivity = completedActivityContext.Activity; - var result = context.Result; - - if (flowchartContext.Activity != this) - { - throw new("Target context activity must be this flowchart"); - } - - // If the completed activity's status is anything but "Completed", do not schedule its outbound activities. - if (completedActivityContext.Status != ActivityStatus.Completed) - { - return; - } - - // If the complete activity is a terminal node, complete the flowchart immediately. - if (completedActivity is ITerminalNode) - { - await flowchartContext.CompleteActivityAsync(); - return; - } - - // Determine the outcomes from the completed activity. - var outcomes = result is Outcomes o ? o : Outcomes.Default; - - // Schedule the outbound activities - var flowGraph = GetFlowGraph(flowchartContext); - var flowScope = GetFlowScope(flowchartContext); - var completedActivityExcecutedByBackwardConnection = completedActivityContext.ActivityInput.GetValueOrDefault(BackwardConnectionActivityInput); - var hasScheduledActivity = await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, completedActivityExcecutedByBackwardConnection); - - // If there are not any outbound connections, complete the flowchart activity if there is no other pending work. - if (!hasScheduledActivity) - { - await CompleteIfNoPendingWorkAsync(flowchartContext); - } - } - - /// - /// Schedules outbound activities based on the flowchart's structure and execution state. - /// This method determines whether an activity should be scheduled based on visited connections, - /// forward traversal rules, and backward connections. - /// - /// The flowchart containing the activities. - /// The graph representation of the flowchart. - /// Tracks activity and connection visits. - /// The execution context of the flowchart. - /// The current activity being processed. - /// The outcomes that determine which connections were followed. - /// Indicates if the completed activity was executed due to a backward connection. - /// True if at least one activity was scheduled; otherwise, false. - private async ValueTask ScheduleOutboundActivitiesAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity activity, Outcomes outcomes, bool completedActivityExecutedByBackwardConnection = false) - { - var hasScheduledActivity = false; - - // Check if the activity is dangling (i.e., it is not reachable from the flowchart graph) - if (flowGraph.IsDanglingActivity(activity)) - { - throw new($"Activity {activity.Id} is not reachable from the flowchart graph. Unable to schedule it's outbound activities."); - } - - // Register the activity as visited unless it was executed due to a backward connection - if (!completedActivityExecutedByBackwardConnection) - { - flowScope.RegisterActivityVisit(activity); - } - - // Process each outbound connection from the current activity - foreach (var outboundConnection in flowGraph.GetOutboundConnections(activity)) - { - var connectionFollowed = outcomes.Names.Contains(outboundConnection.Source.Port); - flowScope.RegisterConnectionVisit(outboundConnection, connectionFollowed); - - if(!connectionFollowed) - continue; // Skip if connection was not followed. - - var outboundActivity = outboundConnection.Target.Activity; - - // Determine the scheduling strategy based on connection-type. - if (flowGraph.IsBackwardConnection(outboundConnection, out var backwardConnectionIsValid)) - { - hasScheduledActivity |= await ScheduleBackwardConnectionActivityAsync(flowGraph, flowchartContext, outboundConnection, outboundActivity, connectionFollowed, backwardConnectionIsValid); - } - else if (outboundActivity is not IJoinNode) - { - hasScheduledActivity |= await ScheduleNonJoinActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity); - } - else - { - hasScheduledActivity |= await ScheduleJoinActivityAsync(flowGraph, flowScope, flowchartContext, outboundConnection, outboundActivity); - } - } - return hasScheduledActivity; - } - - /// - /// Schedules an outbound activity that originates from a backward connection. - /// - private async ValueTask ScheduleBackwardConnectionActivityAsync(FlowGraph flowGraph, ActivityExecutionContext flowchartContext, Connection outboundConnection, IActivity outboundActivity, bool connectionFollowed, bool backwardConnectionIsValid) - { - if (!connectionFollowed) - { - return false; - } - - if (!backwardConnectionIsValid) - { - throw new($"Invalid backward connection: Every path from the source ('{outboundConnection.Source.Activity.Id}') must go through the target ('{outboundConnection.Target.Activity.Id}') when tracing back to the start."); - } - - var scheduleWorkOptions = new ScheduleWorkOptions - { - CompletionCallback = OnChildCompletedAsync, - Input = new Dictionary() { { BackwardConnectionActivityInput, true } } - }; - - await flowchartContext.ScheduleActivityAsync(outboundActivity, scheduleWorkOptions); - return true; - } - - /// - /// Schedules a non-join activity if all its forward inbound connections have been visited. - /// - private async ValueTask ScheduleNonJoinActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity outboundActivity) - { - if (!flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity)) - { - return false; - } - - if (flowScope.HasFollowedInboundConnection(flowGraph, outboundActivity)) - { - await flowchartContext.ScheduleActivityAsync(outboundActivity, OnChildCompletedAsync); - return true; - } - else - { - // Propagate skipped connections by scheduling with Outcomes.Empty - return await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, outboundActivity, Outcomes.Empty); - } - } - - /// - /// Schedules a join activity based on inbound connection statuses. - /// - private async ValueTask ScheduleJoinActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, Connection outboundConnection, IActivity outboundActivity) - { - // Ignore the connection if the join activity has already completed (JoinAny scenario) - if (flowScope.ShouldIgnoreConnection(outboundConnection, outboundActivity)) - { - return false; - } - - // Schedule the join activity only if at least one inbound connection was followed - if (!flowScope.HasFollowedInboundConnection(flowGraph, outboundActivity)) - { - if (flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity)) - { - // Propagate skipped connections by scheduling with Outcomes.Empty - return await ScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, outboundActivity, Outcomes.Empty); - } - return false; - } - - // Check for an existing execution context for the join activity - var joinContext = flowchartContext.WorkflowExecutionContext.ActivityExecutionContexts.LastOrDefault(x => - x.ParentActivityExecutionContext == flowchartContext && - x.Activity == outboundActivity && - x.Status is ActivityStatus.Pending or ActivityStatus.Running); - - // If the join activity was already scheduled, do not schedule it again - if (joinContext == null) - { - var activityScheduled = flowchartContext.WorkflowExecutionContext.Scheduler.List().Any(workItem => workItem.Owner == flowchartContext && workItem.Activity == outboundActivity); - if (activityScheduled) - { - return true; - } - } - - var scheduleWorkOptions = new ScheduleWorkOptions - { - CompletionCallback = OnChildCompletedAsync, - ExistingActivityExecutionContext = joinContext - }; - await flowchartContext.ScheduleActivityAsync(outboundActivity, scheduleWorkOptions); - return true; - } - - public static bool CanWaitAllProceed(ActivityExecutionContext context) - { - var flowchartContext = context.ParentActivityExecutionContext!; - var flowchart = (Flowchart)flowchartContext.Activity; - var flowGraph = flowchart.GetFlowGraph(flowchartContext); - var flowScope = flowchart.GetFlowScope(flowchartContext); - var activity = context.Activity; - - return flowScope.AllInboundConnectionsVisited(flowGraph, activity); - } - - public static async void CancelAncestorActivatesAsync(ActivityExecutionContext context) - { - var flowchartContext = context.ParentActivityExecutionContext!; - var flowchart = (Flowchart)flowchartContext.Activity; - var flowGraph = flowchart.GetFlowGraph(flowchartContext); - var ancestorActivities = flowGraph.GetAncestorActivities(context.Activity); - var inboundActivityExecutionContexts = context.WorkflowExecutionContext.ActivityExecutionContexts.Where(x => ancestorActivities.Contains(x.Activity) && x.ParentActivityExecutionContext == flowchartContext).ToList(); - - // Cancel each ancestor activity. - foreach (var activityExecutionContext in inboundActivityExecutionContexts) - { - await activityExecutionContext.CancelActivityAsync(); - } - } - - private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context) - { - var hasPendingWork = HasPendingWork(context); - - if (!hasPendingWork) - { - var hasFaultedActivities = context.Children.Any(x => x.Status == ActivityStatus.Faulted); - - if (!hasFaultedActivities) - { - await context.CompleteActivityAsync(); - } - } - } - - private async ValueTask OnScheduleOutcomesAsync(ScheduleActivityOutcomes signal, SignalContext context) - { - var flowchartContext = context.ReceiverActivityExecutionContext; - var schedulingActivityContext = context.SenderActivityExecutionContext; - var schedulingActivity = schedulingActivityContext.Activity; - var outcomes = signal.Outcomes; - var outboundConnections = Connections.Where(connection => connection.Source.Activity == schedulingActivity && outcomes.Contains(connection.Source.Port!)).ToList(); - var outboundActivities = outboundConnections.Select(x => x.Target.Activity).ToList(); - - if (outboundActivities.Any()) - { - // Schedule each child. - foreach (var activity in outboundActivities) await flowchartContext.ScheduleActivityAsync(activity, OnChildCompletedAsync); - } - } - private async ValueTask OnScheduleChildActivityAsync(ScheduleChildActivity signal, SignalContext context) { var flowchartContext = context.ReceiverActivityExecutionContext;