diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs
index b056f5010..b7c7dc84f 100644
--- a/src/apps/Elsa.Server.Web/Program.cs
+++ b/src/apps/Elsa.Server.Web/Program.cs
@@ -12,6 +12,7 @@ using Elsa.Tenants.AspNetCore;
using Elsa.Tenants.Extensions;
using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions;
using Elsa.Workflows;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Api;
using Elsa.Workflows.CommitStates.Strategies;
using Elsa.Workflows.IncidentStrategies;
@@ -60,6 +61,7 @@ services
strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
});
})
+ .UseFlowchart(flowchart => flowchart.UseCounterBasedExecution())
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef => ef.UseSqlite());
diff --git a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs
index 818f7787e..5fdb20243 100644
--- a/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs
+++ b/src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs
@@ -20,70 +20,76 @@ namespace Elsa.Testing.Shared;
[PublicAPI]
public static class RunWorkflowExtensions
{
- ///
- /// Runs a workflow until its end, automatically resuming any bookmark it encounters.
- ///
/// The services.
- /// The ID of the workflow definition.
- /// An optional dictionary of input values.
- /// An optional correlation id of the workflow.
- /// An optional set of options to specify the version of the workflow definition to retrieve.
- /// The workflow state.
- public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services,
- string workflowDefinitionId,
- IDictionary? input = null,
- string? correlationId = null,
- VersionOptions? versionOptions = null)
+ extension(IServiceProvider services)
{
- var workflowDefinitionService = services.GetRequiredService();
- var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published);
-
- if (workflowGraph == null)
- throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found.");
-
- var workflowRuntime = services.GetRequiredService();
- var workflowClient = await workflowRuntime.CreateClientAsync();
- var response = await workflowClient.CreateAndRunInstanceAsync(new()
+ ///
+ /// Runs a workflow until its end, automatically resuming any bookmark it encounters.
+ ///
+ /// The ID of the workflow definition.
+ /// An optional dictionary of input values.
+ /// An optional correlation id of the workflow.
+ /// An optional set of options to specify the version of the workflow definition to retrieve.
+ /// Optional workflow execution options.
+ /// The workflow state.
+ public async Task RunWorkflowUntilEndAsync(string workflowDefinitionId,
+ IDictionary? input = null,
+ string? correlationId = null,
+ VersionOptions? versionOptions = null,
+ RunWorkflowOptions? runWorkflowOptions = null)
{
- WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id),
- Input = input,
- CorrelationId = correlationId
- });
-
- var bookmarkStore = services.GetRequiredService();
+ var workflowDefinitionService = services.GetRequiredService();
+ var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, versionOptions ?? VersionOptions.Published);
- // Continue resuming the workflow for as long as there are bookmarks to resume and the workflow is not Finished.
- while (response.Status != WorkflowStatus.Finished)
- {
- var bookmarks = (await bookmarkStore.FindManyAsync(new()
+ if (workflowGraph == null)
+ throw new InvalidOperationException($"Workflow definition with ID '{workflowDefinitionId}' not found.");
+
+ var workflowRuntime = services.GetRequiredService();
+ var workflowClient = await workflowRuntime.CreateClientAsync();
+ var response = await workflowClient.CreateAndRunInstanceAsync(new()
{
- WorkflowInstanceId = response.WorkflowInstanceId
- })).ToList();
+ WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowGraph.Workflow.Identity.Id),
+ Input = input,
+ CorrelationId = correlationId,
+ Properties = runWorkflowOptions?.Properties
+ });
- if (!bookmarks.Any())
- break;
+ var bookmarkStore = services.GetRequiredService();
- foreach (var bookmark in bookmarks)
+ // Continue resuming the workflow for as long as there are bookmarks to resume and the workflow is not Finished.
+ while (response.Status != WorkflowStatus.Finished)
{
- var runRequest = new RunWorkflowInstanceRequest
+ var bookmarks = (await bookmarkStore.FindManyAsync(new()
{
- BookmarkId = bookmark.Id,
- Input = input
- };
- response = await workflowClient.RunInstanceAsync(runRequest);
+ WorkflowInstanceId = response.WorkflowInstanceId
+ })).ToList();
+
+ if (!bookmarks.Any())
+ break;
+
+ foreach (var bookmark in bookmarks)
+ {
+ var runRequest = new RunWorkflowInstanceRequest
+ {
+ BookmarkId = bookmark.Id,
+ Input = input,
+ Properties = runWorkflowOptions?.Properties
+ };
+ response = await workflowClient.RunInstanceAsync(runRequest);
+ }
}
+
+ // Return the workflow state.
+ return await workflowClient.ExportStateAsync();
}
- // Return the workflow state.
- return await workflowClient.ExportStateAsync();
- }
-
- ///
- /// Runs a workflow until its end, automatically resuming any bookmark it encounters.
- ///
- public static async Task RunWorkflowUntilEndAsync(this IServiceProvider services, IDictionary? input = null) where TWorkflow : IWorkflow
- {
- var workflowDefinitionId = typeof(TWorkflow).Name;
- return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
+ ///
+ /// Runs a workflow until its end, automatically resuming any bookmark it encounters.
+ ///
+ public async Task RunWorkflowUntilEndAsync(IDictionary? input = null) where TWorkflow : IWorkflow
+ {
+ var workflowDefinitionId = typeof(TWorkflow).Name;
+ return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
+ }
}
}
\ No newline at end of file
diff --git a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs
index b4dfd6eb8..18cbcfd79 100644
--- a/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs
+++ b/src/common/Elsa.Testing.Shared.Integration/ServiceProviderExtensions.cs
@@ -20,51 +20,52 @@ namespace Elsa.Testing.Shared;
[PublicAPI]
public static class ServiceProviderExtensions
{
- ///
- /// Updates the registries.
- ///
/// The services.
- public static Task PopulateRegistriesAsync(this IServiceProvider services)
+ extension(IServiceProvider services)
{
- var registriesPopulator = services.GetRequiredService();
- return registriesPopulator.PopulateAsync();
- }
-
- ///
- /// Imports a workflow definition from a file.
- ///
- /// The services.
- /// The file name.
- /// The workflow definition.
- public static async Task ImportWorkflowDefinitionAsync(this IServiceProvider services, string fileName)
- {
- var json = await File.ReadAllTextAsync(fileName);
- var serializer = services.GetRequiredService();
- var model = serializer.Deserialize(json);
-
- var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
+ ///
+ /// Updates the registries.
+ ///
+ public Task PopulateRegistriesAsync()
{
- Model = model,
- Publish = true
- };
+ var registriesPopulator = services.GetRequiredService();
+ return registriesPopulator.PopulateAsync();
+ }
- var workflowDefinitionImporter = services.GetRequiredService();
- var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
- return result.WorkflowDefinition;
- }
+ ///
+ /// Imports a workflow definition from a file.
+ ///
+ /// The file name.
+ /// The workflow definition.
+ public async Task ImportWorkflowDefinitionAsync(string fileName)
+ {
+ var json = await File.ReadAllTextAsync(fileName);
+ var serializer = services.GetRequiredService();
+ var model = serializer.Deserialize(json);
- ///
- /// Retrieves a workflow definition by its ID.
- ///
- /// The service provider.
- /// The definition ID of the workflow definition.
- /// Options to specify the version of the workflow definition to retrieve.
- /// A cancellation token to cancel the operation.
- /// The retrieved workflow definition.
- public static async Task GetWorkflowDefinitionAsync(this IServiceProvider services, string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
- {
- var workflowDefinitionService = services.GetRequiredService();
- var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
- return workflowDefinition!;
+ var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
+ {
+ Model = model,
+ Publish = true
+ };
+
+ var workflowDefinitionImporter = services.GetRequiredService();
+ var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
+ return result.WorkflowDefinition;
+ }
+
+ ///
+ /// Retrieves a workflow definition by its ID.
+ ///
+ /// The definition ID of the workflow definition.
+ /// Options to specify the version of the workflow definition to retrieve.
+ /// A cancellation token to cancel the operation.
+ /// The retrieved workflow definition.
+ public async Task GetWorkflowDefinitionAsync(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
+ {
+ var workflowDefinitionService = services.GetRequiredService();
+ var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
+ return workflowDefinition!;
+ }
}
}
\ No newline at end of file
diff --git a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
index 6e4c9ecda..a85e765db 100644
--- a/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
+++ b/src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
@@ -2,8 +2,10 @@ using Elsa.Expressions.Models;
using Elsa.Features.Services;
using Elsa.Workflows;
using Elsa.Workflows.Activities;
+using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
+using Elsa.Workflows.Options;
using Elsa.Workflows.State;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -102,6 +104,9 @@ public class WorkflowTestFixture
///
public async Task BuildAsync()
{
+ if (_services != null)
+ return this;
+
_services = _testApplicationBuilder.Build();
await Services.PopulateRegistriesAsync();
return this;
@@ -116,12 +121,52 @@ public class WorkflowTestFixture
/// The workflow result after execution
public async Task RunWorkflowAsync(IWorkflow workflow, CancellationToken cancellationToken = default)
{
- if (_services == null)
- await BuildAsync();
-
+ await BuildAsync();
var workflowRunner = Services.GetRequiredService();
return await workflowRunner.RunAsync(workflow, cancellationToken: cancellationToken);
}
+
+ ///
+ /// Runs the specified workflow and returns the workflow result.
+ /// Automatically builds the fixture if not already built.
+ ///
+ /// Cancellation token
+ /// The workflow result after execution
+ public async Task RunWorkflowAsync(CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
+ {
+ await BuildAsync();
+ var workflowRunner = Services.GetRequiredService();
+ return await workflowRunner.RunAsync(cancellationToken: cancellationToken);
+ }
+
+ ///
+ /// Runs a workflow with the specified options and returns the workflow result.
+ /// Automatically builds the fixture if not already built.
+ ///
+ /// The workflow to run
+ /// Workflow execution options
+ /// Cancellation token
+ /// The workflow result after execution
+ public async Task RunWorkflowAsync(IWorkflow workflow, RunWorkflowOptions options, CancellationToken cancellationToken = default)
+ {
+ await BuildAsync();
+ var workflowRunner = Services.GetRequiredService();
+ return await workflowRunner.RunAsync(workflow, options, cancellationToken);
+ }
+
+ ///
+ /// Runs the specified workflow with the specified options and returns the workflow result.
+ /// Automatically builds the fixture if not already built.
+ ///
+ /// Workflow execution options
+ /// Cancellation token
+ /// The workflow result after execution
+ public async Task RunWorkflowAsync(RunWorkflowOptions options, CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
+ {
+ await BuildAsync();
+ var workflowRunner = Services.GetRequiredService();
+ return await workflowRunner.RunAsync(options, cancellationToken);
+ }
///
/// Runs an activity wrapped in a workflow and returns the workflow result.
@@ -132,26 +177,45 @@ public class WorkflowTestFixture
/// The workflow result after execution
public async Task RunActivityAsync(IActivity activity, CancellationToken cancellationToken = default)
{
- if (_services == null)
- await BuildAsync();
-
+ await BuildAsync();
var workflowRunner = Services.GetRequiredService();
return await workflowRunner.RunAsync(activity, cancellationToken: cancellationToken);
}
+ ///
+ /// Runs an activity wrapped in a workflow with the specified options and returns the workflow result.
+ /// Automatically builds the fixture if not already built.
+ ///
+ /// The activity to run
+ /// Workflow execution options
+ /// Cancellation token
+ /// The workflow result after execution
+ public async Task RunActivityAsync(IActivity activity, RunWorkflowOptions options, CancellationToken cancellationToken = default)
+ {
+ await BuildAsync();
+ var workflowRunner = Services.GetRequiredService();
+ return await workflowRunner.RunAsync(activity, options, cancellationToken);
+ }
+
///
/// Runs a workflow by definition ID and returns the workflow state.
/// Automatically builds the fixture if not already built.
///
/// The workflow definition ID
/// Optional input dictionary
+ /// Optional workflow execution options
/// The workflow state after execution
- public async Task RunWorkflowAsync(string definitionId, IDictionary? input = null)
+ public async Task RunWorkflowAsync(string definitionId, IDictionary? input = null, RunWorkflowOptions? options = null)
{
- if (_services == null)
- await BuildAsync();
+
+ await BuildAsync();
+ return await Services.RunWorkflowUntilEndAsync(definitionId, input, runWorkflowOptions: options);
+ }
- return await Services.RunWorkflowUntilEndAsync(definitionId, input);
+ public async Task ImportWorkflowDefinitionAsync(string fileName)
+ {
+ await BuildAsync();
+ return await Services.ImportWorkflowDefinitionAsync(fileName);
}
///
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 8fa1c5d06..08fee2028 100644
--- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs
@@ -1,8 +1,11 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Activities.Flowchart.Options;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Signals;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
namespace Elsa.Workflows.Activities.Flowchart.Activities;
@@ -14,9 +17,17 @@ namespace Elsa.Workflows.Activities.Flowchart.Activities;
public partial class Flowchart : Container
{
///
- /// Set this to false from your program file in case you wish to use the old counter based model.
+ /// The property key used to store the flowchart execution mode in .
///
- public static bool UseTokenFlow = true;
+ public const string ExecutionModePropertyKey = "Flowchart:ExecutionMode";
+
+ ///
+ /// Set this to false from your program file in case you wish to use the old counter based model.
+ /// This static field is used as a final fallback when no execution mode is specified via options or workflow execution context properties.
+ /// Note: Prefer using configured via DI for application-wide settings.
+ ///
+ // ReSharper disable once GrammarMistakeInComment
+ public static bool UseTokenFlow = false; // Default to false to maintain the same behavior with 3.5.2 out of the box.
///
public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
@@ -29,7 +40,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.
@@ -78,16 +89,18 @@ public partial class Flowchart : Container
private ValueTask OnChildCompletedAsync(ActivityCompletedContext context)
{
- return UseTokenFlow
- ? OnChildCompletedTokenBasedLogicAsync(context)
- : OnChildCompletedCounterBasedLogicAsync(context);
+ return ExecuteBasedOnMode(
+ context.TargetContext,
+ () => OnChildCompletedTokenBasedLogicAsync(context),
+ () => OnChildCompletedCounterBasedLogicAsync(context));
}
private ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context)
{
- return UseTokenFlow
- ? OnTokenFlowActivityCanceledAsync(signal, context)
- : OnCounterFlowActivityCanceledAsync(signal, context);
+ return ExecuteBasedOnMode(
+ context.ReceiverActivityExecutionContext,
+ () => OnTokenFlowActivityCanceledAsync(signal, context),
+ () => OnCounterFlowActivityCanceledAsync(signal, context));
}
private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context)
@@ -104,4 +117,50 @@ public partial class Flowchart : Container
}
}
}
+
+ private static ValueTask ExecuteBasedOnMode(ActivityExecutionContext context, Func tokenBasedAction, Func counterBasedAction)
+ {
+ var mode = GetEffectiveExecutionMode(context);
+
+ return mode switch
+ {
+ FlowchartExecutionMode.TokenBased => tokenBasedAction(),
+ FlowchartExecutionMode.CounterBased or FlowchartExecutionMode.Default or _ => counterBasedAction()
+ };
+ }
+
+ ///
+ /// Gets the effective execution mode for this flowchart execution.
+ /// Priority: WorkflowExecutionContext.Properties > FlowchartOptions (DI) > Static UseTokenFlow flag
+ ///
+ private static FlowchartExecutionMode GetEffectiveExecutionMode(ActivityExecutionContext context)
+ {
+ var workflowExecutionContext = context.WorkflowExecutionContext;
+
+ if (!workflowExecutionContext.Properties.TryGetValue(ExecutionModePropertyKey, out var modeValue))
+ return GetDefaultModeFromOptions(context);
+
+ var mode = ParseExecutionMode(modeValue);
+ return mode != FlowchartExecutionMode.Default ? mode : GetDefaultModeFromOptions(context);
+ }
+
+ private static FlowchartExecutionMode ParseExecutionMode(object modeValue)
+ {
+ return modeValue switch
+ {
+ FlowchartExecutionMode executionMode => executionMode,
+ string str when Enum.TryParse(str, true, out var parsed) => parsed,
+ int intValue when Enum.IsDefined(typeof(FlowchartExecutionMode), intValue) => (FlowchartExecutionMode)intValue,
+ _ => FlowchartExecutionMode.Default
+ };
+ }
+
+ private static FlowchartExecutionMode GetDefaultModeFromOptions(ActivityExecutionContext context)
+ {
+ var options = context.WorkflowExecutionContext.ServiceProvider.GetService>();
+ var mode = options?.Value.DefaultExecutionMode ?? FlowchartExecutionMode.Default;
+ if (mode == FlowchartExecutionMode.Default)
+ return UseTokenFlow ? FlowchartExecutionMode.TokenBased : FlowchartExecutionMode.CounterBased;
+ return mode;
+ }
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs
new file mode 100644
index 000000000..dae0c3af6
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/FlowchartFeatureExtensions.cs
@@ -0,0 +1,47 @@
+using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Activities.Flowchart.Options;
+using Elsa.Workflows.Features;
+
+namespace Elsa.Workflows.Activities.Flowchart.Extensions;
+
+///
+/// Extension methods for .
+///
+public static class FlowchartFeatureExtensions
+{
+ extension(FlowchartFeature feature)
+ {
+ ///
+ /// Configures the flowchart options.
+ ///
+ public FlowchartFeature ConfigureFlowchart(Action configure)
+ {
+ feature.FlowchartOptionsConfigurator = configure;
+ return feature;
+ }
+
+ ///
+ /// Sets the default execution mode for flowcharts to token-based.
+ ///
+ public FlowchartFeature UseTokenBasedExecution()
+ {
+ return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.TokenBased);
+ }
+
+ ///
+ /// Sets the default execution mode for flowcharts to counter-based (legacy mode).
+ ///
+ public FlowchartFeature UseCounterBasedExecution()
+ {
+ return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.CounterBased);
+ }
+
+ ///
+ /// Sets the default execution mode for flowcharts to the specified mode.
+ ///
+ public FlowchartFeature UseExecution(FlowchartExecutionMode mode)
+ {
+ return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = mode);
+ }
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs
new file mode 100644
index 000000000..fce338735
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extensions/RunWorkflowOptionsExtensions.cs
@@ -0,0 +1,39 @@
+using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Options;
+
+namespace Elsa.Workflows.Activities.Flowchart.Extensions;
+
+///
+/// Extension methods for to configure flowchart execution mode.
+///
+public static class RunWorkflowOptionsExtensions
+{
+ extension(RunWorkflowOptions options)
+ {
+ ///
+ /// Sets the flowchart execution mode to token-based.
+ ///
+ public RunWorkflowOptions WithTokenBasedFlowchart()
+ {
+ return options.WithFlowchartExecutionMode(FlowchartExecutionMode.TokenBased);
+ }
+
+ ///
+ /// Sets the flowchart execution mode to counter-based (legacy mode).
+ ///
+ public RunWorkflowOptions WithCounterBasedFlowchart()
+ {
+ return options.WithFlowchartExecutionMode(FlowchartExecutionMode.CounterBased);
+ }
+
+ ///
+ /// Sets the flowchart execution mode.
+ ///
+ public RunWorkflowOptions WithFlowchartExecutionMode(FlowchartExecutionMode mode)
+ {
+ options.Properties ??= new Dictionary();
+ options.Properties[Activities.Flowchart.ExecutionModePropertyKey] = mode;
+ return options;
+ }
+ }
+}
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs
new file mode 100644
index 000000000..0368f0c70
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs
@@ -0,0 +1,22 @@
+namespace Elsa.Workflows.Activities.Flowchart.Models;
+
+///
+/// Specifies the execution mode for flowchart activities.
+///
+public enum FlowchartExecutionMode
+{
+ ///
+ /// Use the default mode as specified by .
+ ///
+ Default = 0,
+
+ ///
+ /// Use token-based flow logic.
+ ///
+ TokenBased = 1,
+
+ ///
+ /// Use counter-based flow logic (legacy mode).
+ ///
+ CounterBased = 2
+}
diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs
new file mode 100644
index 000000000..db1e92d62
--- /dev/null
+++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/FlowchartOptions.cs
@@ -0,0 +1,15 @@
+using Elsa.Workflows.Activities.Flowchart.Models;
+
+namespace Elsa.Workflows.Activities.Flowchart.Options;
+
+///
+/// Options for configuring flowchart execution behavior.
+///
+public class FlowchartOptions
+{
+ ///
+ /// Gets or sets the default execution mode for flowcharts when not explicitly specified.
+ /// Defaults to .
+ ///
+ public FlowchartExecutionMode DefaultExecutionMode { get; set; } = FlowchartExecutionMode.CounterBased; // Default to counter-based in order to maintain the same behavior with 3.5.2 out of the box.
+}
diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
index ccd559f5c..e456d0d39 100644
--- a/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
+++ b/src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
@@ -6,9 +6,18 @@ namespace Elsa.Extensions;
public static class ModuleExtensions
{
- public static IModule UseWorkflows(this IModule configuration, Action? configure = default)
+ extension(IModule configuration)
{
- configuration.Configure(configure);
- return configuration;
+ public IModule UseWorkflows(Action? configure = null)
+ {
+ configuration.Configure(configure);
+ return configuration;
+ }
+
+ public IModule UseFlowchart(Action? configure = null)
+ {
+ configuration.Configure(configure);
+ return configuration;
+ }
}
}
\ No newline at end of file
diff --git a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
index f489487bd..ab3a000a2 100644
--- a/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
+++ b/src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
@@ -2,7 +2,9 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Activities.Flowchart.Options;
using Elsa.Workflows.Activities.Flowchart.Serialization;
+using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Features;
@@ -16,11 +18,21 @@ public class FlowchartFeature : FeatureBase
{
}
+ ///
+ /// A delegate to configure .
+ ///
+ public Action? FlowchartOptionsConfigurator { get; set; }
+
///
public override void Apply()
{
Services.AddSerializationOptionsConfigurator();
-
+
+ // Register FlowchartOptions
+ Services.AddOptions();
+
+ if (FlowchartOptionsConfigurator != null)
+ Services.Configure(FlowchartOptionsConfigurator);
}
public override void Configure()
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs
index 1853ca250..81f9b0dc8 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs
+++ b/test/integration/Elsa.Activities.IntegrationTests/Branching/FlowDecisionTests.cs
@@ -1,6 +1,9 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
+using Elsa.Workflows.Activities.Flowchart.Models;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests.Branching;
@@ -9,23 +12,15 @@ namespace Elsa.Activities.IntegrationTests.Branching;
/// Integration tests for FlowDecision activity in flowchart scenarios.
///
[Collection("FlowchartTests")]
-public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable
+public class FlowDecisionTests(ITestOutputHelper testOutputHelper)
{
private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
- private readonly bool _originalFlowMode = Flowchart.UseTokenFlow;
-
- public void Dispose()
- {
- Flowchart.UseTokenFlow = _originalFlowMode;
- }
[Theory(DisplayName = "FlowDecision follows correct path based on condition")]
[MemberData(nameof(BasicPathTestCases))]
- public async Task Should_Follow_Correct_Path_Based_On_Condition(bool useTokenFlow, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
+ public async Task Should_Follow_Correct_Path_Based_On_Condition(FlowchartExecutionMode executionMode, bool condition, string[] expectedOutputs, string[] unexpectedOutputs)
{
// Arrange
- Flowchart.UseTokenFlow = useTokenFlow;
-
var start = new WriteLine("Start");
var decision = new FlowDecision(ctx => condition);
var truePath = new WriteLine("TruePath");
@@ -43,8 +38,10 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable
}
};
+ var options = new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode);
+
// Act
- await _fixture.RunActivityAsync(flowchart);
+ await _fixture.RunActivityAsync(flowchart, options);
// Assert
AssertOutputs(expectedOutputs, unexpectedOutputs);
@@ -53,19 +50,17 @@ public class FlowDecisionTests(ITestOutputHelper testOutputHelper) : IDisposable
public static IEnumerable
[Collection("FlowchartTests")]
-public class FlowchartCounterBasedTests : IDisposable
+public class FlowchartCounterBasedTests
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output;
- private readonly bool _originalFlowMode;
public FlowchartCounterBasedTests(ITestOutputHelper testOutputHelper)
{
_output = new();
_services = CreateServiceProvider(testOutputHelper, _output);
- _originalFlowMode = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = false;
- }
-
- public void Dispose()
- {
- Flowchart.UseTokenFlow = _originalFlowMode;
}
[Fact(DisplayName = "Executes simple linear flowchart")]
@@ -41,7 +33,7 @@ public class FlowchartCounterBasedTests : IDisposable
);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Equal(3, _output.Lines.Count);
@@ -60,7 +52,7 @@ public class FlowchartCounterBasedTests : IDisposable
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Equal(3, _output.Lines.Count);
@@ -81,7 +73,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Single(_output.Lines);
@@ -98,7 +90,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- var result = await RunFlowchartAsync(_services, flowchart);
+ var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.NotNull(result);
@@ -122,7 +114,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Single(_output.Lines);
@@ -154,7 +146,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -188,7 +180,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -229,7 +221,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -269,7 +261,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -292,7 +284,7 @@ public class FlowchartCounterBasedTests : IDisposable
);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Equal(4, _output.Lines.Count);
@@ -342,7 +334,7 @@ public class FlowchartCounterBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.CounterBased);
// Assert
Assert.Single(_output.Lines);
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs
index c5ee60959..db8384904 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs
+++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTestHelpers.cs
@@ -1,8 +1,10 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Models;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Activities.IntegrationTests.Flow;
@@ -20,9 +22,13 @@ public static class FlowchartTestHelpers
return builder.Build();
}
- public static async Task RunFlowchartAsync(IServiceProvider services, Flowchart flowchart)
+ public static async Task RunFlowchartAsync(IServiceProvider services, Flowchart flowchart, FlowchartExecutionMode? executionMode = null)
{
- return await services.RunActivityAsync(flowchart);
+ var options = executionMode.HasValue
+ ? new RunWorkflowOptions().WithFlowchartExecutionMode(executionMode.Value)
+ : null;
+
+ return await services.RunActivityAsync(flowchart, options);
}
public static Connection CreateConnection(IActivity source, IActivity target, string? outcome = "Done")
diff --git a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs
index b62d27882..e6ff586e9 100644
--- a/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs
+++ b/test/integration/Elsa.Activities.IntegrationTests/Flow/FlowchartTokenBasedTests.cs
@@ -12,23 +12,15 @@ namespace Elsa.Activities.IntegrationTests.Flow;
/// Integration tests for token-based flowchart execution strategy.
///
[Collection("FlowchartTests")]
-public class FlowchartTokenBasedTests : IDisposable
+public class FlowchartTokenBasedTests
{
private readonly IServiceProvider _services;
private readonly CapturingTextWriter _output;
- private readonly bool _originalFlowMode;
public FlowchartTokenBasedTests(ITestOutputHelper testOutputHelper)
{
_output = new();
_services = CreateServiceProvider(testOutputHelper, _output);
- _originalFlowMode = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = true;
- }
-
- public void Dispose()
- {
- Flowchart.UseTokenFlow = _originalFlowMode;
}
[Fact(DisplayName = "Executes simple linear flowchart")]
@@ -42,7 +34,7 @@ public class FlowchartTokenBasedTests : IDisposable
);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Equal(3, _output.Lines.Count);
@@ -61,7 +53,7 @@ public class FlowchartTokenBasedTests : IDisposable
var flowchart = CreateBranchingFlowchart(start, branch1, branch2);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Equal(3, _output.Lines.Count);
@@ -82,7 +74,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Single(_output.Lines);
@@ -99,7 +91,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- var result = await RunFlowchartAsync(_services, flowchart);
+ var result = await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.NotNull(result);
@@ -123,7 +115,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Single(_output.Lines);
@@ -154,7 +146,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -186,7 +178,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -220,7 +212,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -254,7 +246,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -274,7 +266,7 @@ public class FlowchartTokenBasedTests : IDisposable
var flowchart = CreateSimpleLinearFlowchart(start, middle, end);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
// Tokens should be consumed after each activity completes
@@ -318,7 +310,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -361,7 +353,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -385,7 +377,7 @@ public class FlowchartTokenBasedTests : IDisposable
);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Equal(4, _output.Lines.Count);
@@ -435,7 +427,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Single(_output.Lines);
@@ -477,7 +469,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -498,7 +490,7 @@ public class FlowchartTokenBasedTests : IDisposable
var flowchart = CreateSimpleLinearFlowchart(start, single, end);
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
Assert.Contains("Start", _output.Lines);
@@ -532,7 +524,7 @@ public class FlowchartTokenBasedTests : IDisposable
};
// Act
- await RunFlowchartAsync(_services, flowchart);
+ await RunFlowchartAsync(_services, flowchart, FlowchartExecutionMode.TokenBased);
// Assert
// Verify all activities executed in a valid order
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs
index 1cb751446..b630e2858 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/FlowchartNextActivity/Tests.cs
@@ -2,40 +2,25 @@ using Elsa.Expressions.Models;
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity.Workflows;
using Elsa.Workflows.Memory;
-using Microsoft.Extensions.DependencyInjection;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.FlowchartNextActivity;
-public class FlowchartNextActivityTests
+public class FlowchartNextActivityTests(ITestOutputHelper testOutputHelper)
{
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
- private readonly IWorkflowRunner _workflowRunner;
-
- public FlowchartNextActivityTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper)
- .WithCapturingTextWriter(_capturingTextWriter)
- .AddActivitiesFrom()
- .Build();
-
- _workflowRunner = _services.GetRequiredService();
- }
+ private readonly WorkflowTestFixture _fixture = new WorkflowTestFixture(testOutputHelper).AddActivitiesFrom();
[Fact(DisplayName = "Flowchart only schedules next activity connected to outcome of previous activity.")]
- public async Task Test1()
+ public async Task FlowchartOnlySchedulesNextConnectedActivity()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
- Assert.Equal(new[]
- {
- "Line 1"
- }, lines);
+ await _fixture.RunWorkflowAsync();
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
+ Assert.Equal(["Line 1"], lines);
}
[Fact(DisplayName = "Flowchart with backward connections and a dangling activity")]
@@ -117,9 +102,8 @@ public class FlowchartNextActivityTests
};
});
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ var result = await _fixture.RunWorkflowAsync(workflow);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
Assert.Equal(new[]
{
@@ -127,12 +111,10 @@ public class FlowchartNextActivityTests
}, lines);
}
- [Fact(DisplayName = "Flowchart with an invalid backward connection")]
+ [Fact(DisplayName = "Flowchart with an invalid backward connection (counter-based mode only)")]
public async Task InvalidBackwardConnectionTest()
{
- if(Flowchart.UseTokenFlow)
- return;
-
+ // This test is only valid for counter-based mode
var workflow = new TestWorkflow(workflowBuilder =>
{
var start = new Start
@@ -184,9 +166,9 @@ public class FlowchartNextActivityTests
};
});
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ var options = new RunWorkflowOptions().WithCounterBasedFlowchart();
+ var result = await _fixture.RunWorkflowAsync(workflow, options);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(WorkflowSubStatus.Faulted, result.WorkflowState.SubStatus);
Assert.Single(result.WorkflowState.Incidents);
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);
@@ -222,13 +204,14 @@ public class FlowchartNextActivityTests
};
var loopbackDecision = new FlowSwitch
{
- Cases = {
+ Cases =
+ {
new("LessThanThree", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
},
Mode = new(SwitchMode.MatchFirst)
};
var end = new End();
-
+
workflowBuilder.Root = new Flowchart
{
Variables =
@@ -257,18 +240,20 @@ public class FlowchartNextActivityTests
new(c, join),
new(d, join),
new(join, incrementLoop),
- new(incrementLoop,loopbackDecision),
+ new(incrementLoop, loopbackDecision),
new(new(loopbackDecision, "LessThanThree"), new Endpoint(a)),
new(new(loopbackDecision, "Default"), new Endpoint(end)),
}
};
});
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ var result = await _fixture.RunWorkflowAsync(workflow);
+ var lines = _fixture.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")]
@@ -284,7 +269,8 @@ public class FlowchartNextActivityTests
var start = new Start();
var loopbackSwitch = new FlowSwitch
{
- Cases = {
+ Cases =
+ {
new("DoLoopback", new Expression("JavaScript", "getVariable('LoopCount') < 3")),
},
Mode = new(SwitchMode.MatchFirst)
@@ -301,7 +287,7 @@ public class FlowchartNextActivityTests
};
var b = new WriteLine("B");
var end = new End();
-
+
workflowBuilder.Root = new Flowchart
{
Variables =
@@ -332,9 +318,8 @@ public class FlowchartNextActivityTests
};
});
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ var result = await _fixture.RunWorkflowAsync(workflow);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
Assert.Equal(new[]
{
@@ -342,13 +327,31 @@ public class FlowchartNextActivityTests
}, lines);
}
- [Theory(DisplayName = "Flowchart Join behaves correctly")]
- [InlineData(false, FlowJoinMode.WaitAll, new[] { "A", "B", "C", "D", "F" })] // "E" is not scheduled because join has an unfollowed inbound connection
- [InlineData(false, FlowJoinMode.WaitAllActive, new[] { "A", "B", "C", "D", "E", "F" })] // "E" gets scheduled by join with an unfollowed inbound connection
- [InlineData(false, FlowJoinMode.WaitAny, new[] { "A", "B", "C", "D", "E", "F" })] // "E" only scheduled once
- [InlineData(true, FlowJoinMode.WaitAll, new[] { "A", "B", "C", "E", "F" })] // all Join inbound connections followed, "E" gets scheduled
- [InlineData(true, FlowJoinMode.WaitAllActive, new[] { "A", "B", "C", "E", "F" })] // all Join inbound connections followed, "E" gets scheduled
- [InlineData(true, FlowJoinMode.WaitAny, new[] { "A", "B", "C", "E", "F" })] // "E" only scheduled once
+ [Theory(DisplayName = "Flowchart Join behaves correctly (counter-based mode)")]
+ [InlineData(false, FlowJoinMode.WaitAll, new[]
+ {
+ "A", "B", "C", "D", "F"
+ })] // "E" is not scheduled because join has an unfollowed inbound connection
+ [InlineData(false, FlowJoinMode.WaitAllActive, new[]
+ {
+ "A", "B", "C", "D", "E", "F"
+ })] // "E" gets scheduled by join with an unfollowed inbound connection
+ [InlineData(false, FlowJoinMode.WaitAny, new[]
+ {
+ "A", "B", "C", "D", "E", "F"
+ })] // "E" only scheduled once
+ [InlineData(true, FlowJoinMode.WaitAll, new[]
+ {
+ "A", "B", "C", "E", "F"
+ })] // all Join inbound connections followed, "E" gets scheduled
+ [InlineData(true, FlowJoinMode.WaitAllActive, new[]
+ {
+ "A", "B", "C", "E", "F"
+ })] // all Join inbound connections followed, "E" gets scheduled
+ [InlineData(true, FlowJoinMode.WaitAny, new[]
+ {
+ "A", "B", "C", "E", "F"
+ })] // "E" only scheduled once
// Start
// / | \
// / | \
@@ -362,54 +365,75 @@ public class FlowchartNextActivityTests
// (false) \ | /
// | Join
// D |
- // \ E
+ // \ E
// \ /
// \ /
- // \ /
+ // \ /
// F
public async Task JoinBehavesCorrectly(bool decisionResult, FlowJoinMode joinMode, string[] expectedLines)
{
- Flowchart.UseTokenFlow = false;
+ // This test validates counter-based mode behavior
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 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 decision = new FlowDecision()
{
Condition = new(new Literal(decisionResult))
};
- var d = new WriteLine("D") { Id = "WriteLineD" };
+ var d = new WriteLine("D")
+ {
+ Id = "WriteLineD"
+ };
var join = new FlowJoin()
{
Mode = new(joinMode)
};
- var e = new WriteLine("E") { Id = "WriteLineE" };
- var f = new WriteLine("F") { Id = "WriteLineF" };
+ var e = new WriteLine("E")
+ {
+ Id = "WriteLineE"
+ };
+ var f = new WriteLine("F")
+ {
+ Id = "WriteLineF"
+ };
workflowBuilder.Root = new Flowchart
{
Activities =
- {
- start,
- a,
- b,
- c,
- decision,
- d,
- join,
- e,
- f,
- },
+ {
+ start,
+ a,
+ b,
+ c,
+ decision,
+ d,
+ join,
+ e,
+ f,
+ },
Connections =
{
new(start, a),
new(start, b),
new(start, c),
new(a, decision),
- new(new Endpoint(decision, "True"), new Endpoint(join)),
- new(new Endpoint(decision, "False"), new Endpoint(d)),
+ new(new(decision, "True"), new Endpoint(join)),
+ new(new(decision, "False"), new Endpoint(d)),
new(b, join),
new(c, join),
new(d, f),
@@ -419,11 +443,10 @@ public class FlowchartNextActivityTests
};
});
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync(workflow);
- var lines = _capturingTextWriter.Lines.ToList();
+ var options = new RunWorkflowOptions().WithCounterBasedFlowchart();
+ var result = await _fixture.RunWorkflowAsync(workflow, options);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus);
Assert.Equal(expectedLines, lines);
- Flowchart.UseTokenFlow = true;
}
}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs
index 5c77b115e..640cde2d1 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/BraidedWorkflowTests.cs
@@ -1,36 +1,29 @@
using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
-using Microsoft.Extensions.DependencyInjection;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
-public class BraidedWorkflowTests
+public class BraidedWorkflowTests(ITestOutputHelper testOutputHelper)
{
- private readonly IWorkflowRunner _workflowRunner;
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
-
- public BraidedWorkflowTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- _workflowRunner = _services.GetRequiredService();
- }
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "Braided workflows are executed correctly")]
public async Task Test1()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ await _fixture.RunWorkflowAsync(options);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "WriteLine1", "WriteLine2", "WriteLine3", "WriteLine4", "WriteLine5", "WriteLine6", "WriteLine7" }, lines);
}
-
+
[Fact(DisplayName = "Braided workflows complete the workflow")]
public async Task Test2()
{
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync();
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ var result = await _fixture.RunWorkflowAsync(options);
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
}
}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs
index 34ad36849..64c544d00 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ForkDecisionJoinTests.cs
@@ -1,17 +1,13 @@
using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
-public class ForkDecisionJoinTests
+public class ForkDecisionJoinTests(ITestOutputHelper testOutputHelper)
{
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
-
- public ForkDecisionJoinTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- }
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "The implicit join configured with Stream merge mode should execute.")]
public async Task ImplicitJoinStreamShouldExecute()
@@ -36,36 +32,32 @@ public class ForkDecisionJoinTests
{
await RunAndAssert("fork-decision-join-waitall.json", ["A", "C", "B", "D"]);
}
-
+
[Fact(DisplayName = "An implicit join from the True and False branches should execute because the join mode is Stream and by default, all active branches are joined.")]
public async Task ImplicitJoinFromBranchesShouldExecute()
{
- // Populate registries.
- await _services.PopulateRegistriesAsync();
-
// Import workflow.
- var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json");
+ var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json");
- // Execute.
- var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
+ // Execute with token-based mode.
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ var workflowState = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options);
// Assert.
Assert.Equal(WorkflowStatus.Finished, workflowState.Status);
}
-
+
private async Task RunAndAssert(string workflowFileName, string[] expectedLines)
{
- // Populate registries.
- await _services.PopulateRegistriesAsync();
-
// Import workflow.
- var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/{workflowFileName}");
+ var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/{workflowFileName}");
+
+ // Execute with token-based mode.
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options);
- // Execute.
- await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
-
// Assert.
- var lines = _capturingTextWriter.Lines.ToList();
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(expectedLines, lines);
}
}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs
index 4d6e5ebbb..16a9661b9 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ImplicitLoopWorkflowTests.cs
@@ -1,36 +1,29 @@
using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
using Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors.Workflows;
-using Microsoft.Extensions.DependencyInjection;
+using Elsa.Workflows.Options;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
-public class ImplicitWorkflowTests
+public class ImplicitWorkflowTests(ITestOutputHelper testOutputHelper)
{
- private readonly IWorkflowRunner _workflowRunner;
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
-
- public ImplicitWorkflowTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- _workflowRunner = _services.GetRequiredService();
- }
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "Implicit loop workflows are executed correctly")]
public async Task Test1()
{
- await _services.PopulateRegistriesAsync();
- await _workflowRunner.RunAsync();
- var lines = _capturingTextWriter.Lines.ToList();
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ await _fixture.RunWorkflowAsync(options);
+ var lines = _fixture.CapturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Start", "Retry", "End" }, lines);
}
-
+
[Fact(DisplayName = "Implicit loop workflows complete the workflow")]
public async Task Test2()
{
- await _services.PopulateRegistriesAsync();
- var result = await _workflowRunner.RunAsync();
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ var result = await _fixture.RunWorkflowAsync(options);
Assert.Equal(WorkflowStatus.Finished, result.WorkflowState.Status);
}
}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs
index 36a60a563..fc3869a22 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/JoinRunsOnceTests.cs
@@ -1,35 +1,29 @@
using Elsa.Common.Models;
using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
+using Elsa.Workflows.Options;
using Elsa.Workflows.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
-public class JoinRunsOnceTests
+public class JoinRunsOnceTests(ITestOutputHelper testOutputHelper)
{
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
-
- public JoinRunsOnceTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- }
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "The Join activity executes only once, not twice")]
public async Task Test1()
{
- // Populate registries.
- await _services.PopulateRegistriesAsync();
-
// Import workflow.
- var workflowDefinition = await _services.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/join.json");
+ var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync($"Scenarios/JoinBehaviors/Workflows/join.json");
+
+ // Execute with token-based mode.
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ var state = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options);
- // Execute.
- var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
-
// Assert.
- var journal = await _services.GetRequiredService().FindManyAsync(new()
+ var journal = await _fixture.Services.GetRequiredService().FindManyAsync(new()
{
WorkflowInstanceId = state.Id,
ActivityId = "802725996be1b582",
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs
index 22c5d8a71..93bef7ef2 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/ParallelJoinCompletesTests.cs
@@ -1,35 +1,29 @@
using Elsa.Common.Models;
using Elsa.Testing.Shared;
+using Elsa.Workflows.Activities.Flowchart.Extensions;
+using Elsa.Workflows.Options;
using Elsa.Workflows.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
namespace Elsa.Workflows.IntegrationTests.Scenarios.JoinBehaviors;
-public class ParallelJoinCompletesTests
+public class ParallelJoinCompletesTests(ITestOutputHelper testOutputHelper)
{
- private readonly CapturingTextWriter _capturingTextWriter = new();
- private readonly IServiceProvider _services;
-
- public ParallelJoinCompletesTests(ITestOutputHelper testOutputHelper)
- {
- _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
- }
+ private readonly WorkflowTestFixture _fixture = new(testOutputHelper);
[Fact(DisplayName = "The ParallelForEach activity completes when its Body contains a Join activity")]
public async Task Test1()
{
- // Populate registries.
- await _services.PopulateRegistriesAsync();
-
// Import workflow.
- var workflowDefinition = await _services.ImportWorkflowDefinitionAsync("Scenarios/JoinBehaviors/Workflows/parallel-join.json");
+ var workflowDefinition = await _fixture.ImportWorkflowDefinitionAsync("Scenarios/JoinBehaviors/Workflows/parallel-join.json");
+
+ // Execute with token-based mode.
+ var options = new RunWorkflowOptions().WithTokenBasedFlowchart();
+ var state = await _fixture.RunWorkflowAsync(workflowDefinition.DefinitionId, options: options);
- // Execute.
- var state = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId);
-
// Assert.
- var journal = await _services.GetRequiredService().FindManyAsync(new()
+ var journal = await _fixture.Services.GetRequiredService().FindManyAsync(new()
{
WorkflowInstanceId = state.Id,
ActivityId = "70fc1183cd5800f2",
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json
index 81820da59..c3b454c63 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/decision-merge-join-none.json
@@ -1 +1,149 @@
-{"$schema":"https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json","id":"c2601be150ec7739","definitionId":"b8c92c080faf734f","name":"Decision Merge","createdAt":"2025-10-08T19:00:25.918638\u002B00:00","version":5,"toolVersion":"3.6.0.0","variables":[],"inputs":[],"outputs":[],"outcomes":[],"customProperties":{},"isReadonly":false,"isSystem":false,"isLatest":true,"isPublished":true,"options":{"autoUpdateConsumingWorkflows":false},"root":{"id":"990d07168c5223de","nodeId":"Workflow1:990d07168c5223de","name":"Flowchart1","type":"Elsa.Flowchart","version":1,"customProperties":{"notFoundConnections":[],"canStartWorkflow":false,"runAsynchronously":false},"metadata":{},"activities":[{"condition":{"typeName":"Boolean","expression":{"type":"Literal","value":false}},"id":"a38215f28410f472","nodeId":"Workflow1:990d07168c5223de:a38215f28410f472","name":"FlowDecision1","type":"Elsa.FlowDecision","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-237.5,"y":-255.5},"size":{"width":149.84375,"height":67.9765625}}}},{"id":"50e042cfab596891","nodeId":"Workflow1:990d07168c5223de:50e042cfab596891","name":"Start1","type":"Elsa.Start","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false},"metadata":{"designer":{"position":{"x":-485.5,"y":-255.5},"size":{"width":122.6484375,"height":67.9765625}}}},{"id":"d5b9c2affe09a93b","nodeId":"Workflow1:990d07168c5223de:d5b9c2affe09a93b","name":"End1","type":"Elsa.End","version":1,"customProperties":{"canStartWorkflow":false,"runAsynchronously":false,"mergeMode":"None"},"metadata":{"designer":{"position":{"x":75.5,"y":-255.5},"size":{"width":115.3125,"height":67.9765625}}}}],"variables":[],"connections":[{"source":{"activity":"50e042cfab596891","port":"Done"},"target":{"activity":"a38215f28410f472","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"True"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]},{"source":{"activity":"a38215f28410f472","port":"False"},"target":{"activity":"d5b9c2affe09a93b","port":"In"},"vertices":[]}]}}
\ No newline at end of file
+{
+ "$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
+ "id": "c2601be150ec7739",
+ "definitionId": "b8c92c080faf734f",
+ "name": "Decision Merge",
+ "createdAt": "2025-10-08T19:00:25.918638\u002B00:00",
+ "version": 5,
+ "toolVersion": "3.6.0.0",
+ "variables": [],
+ "inputs": [],
+ "outputs": [],
+ "outcomes": [],
+ "customProperties": {},
+ "isReadonly": false,
+ "isSystem": false,
+ "isLatest": true,
+ "isPublished": true,
+ "options": {
+ "autoUpdateConsumingWorkflows": false
+ },
+ "root": {
+ "id": "990d07168c5223de",
+ "nodeId": "Workflow1:990d07168c5223de",
+ "name": "Flowchart1",
+ "type": "Elsa.Flowchart",
+ "version": 1,
+ "customProperties": {
+ "notFoundConnections": [],
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {},
+ "activities": [
+ {
+ "condition": {
+ "typeName": "Boolean",
+ "expression": {
+ "type": "Literal",
+ "value": false
+ }
+ },
+ "id": "a38215f28410f472",
+ "nodeId": "Workflow1:990d07168c5223de:a38215f28410f472",
+ "name": "FlowDecision1",
+ "type": "Elsa.FlowDecision",
+ "version": 1,
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {
+ "designer": {
+ "position": {
+ "x": -237.5,
+ "y": -255.5
+ },
+ "size": {
+ "width": 149.84375,
+ "height": 67.9765625
+ }
+ }
+ }
+ },
+ {
+ "id": "50e042cfab596891",
+ "nodeId": "Workflow1:990d07168c5223de:50e042cfab596891",
+ "name": "Start1",
+ "type": "Elsa.Start",
+ "version": 1,
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false
+ },
+ "metadata": {
+ "designer": {
+ "position": {
+ "x": -485.5,
+ "y": -255.5
+ },
+ "size": {
+ "width": 122.6484375,
+ "height": 67.9765625
+ }
+ }
+ }
+ },
+ {
+ "id": "d5b9c2affe09a93b",
+ "nodeId": "Workflow1:990d07168c5223de:d5b9c2affe09a93b",
+ "name": "End1",
+ "type": "Elsa.End",
+ "version": 1,
+ "customProperties": {
+ "canStartWorkflow": false,
+ "runAsynchronously": false,
+ "mergeMode": "None"
+ },
+ "metadata": {
+ "designer": {
+ "position": {
+ "x": 75.5,
+ "y": -255.5
+ },
+ "size": {
+ "width": 115.3125,
+ "height": 67.9765625
+ }
+ }
+ }
+ }
+ ],
+ "variables": [],
+ "connections": [
+ {
+ "source": {
+ "activity": "50e042cfab596891",
+ "port": "Done"
+ },
+ "target": {
+ "activity": "a38215f28410f472",
+ "port": "In"
+ },
+ "vertices": []
+ },
+ {
+ "source": {
+ "activity": "a38215f28410f472",
+ "port": "True"
+ },
+ "target": {
+ "activity": "d5b9c2affe09a93b",
+ "port": "In"
+ },
+ "vertices": []
+ },
+ {
+ "source": {
+ "activity": "a38215f28410f472",
+ "port": "False"
+ },
+ "target": {
+ "activity": "d5b9c2affe09a93b",
+ "port": "In"
+ },
+ "vertices": []
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json
index 40ca9c9f2..b64e10f84 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge-strict.json
@@ -1,7 +1,7 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "strict-converge-test",
- "definitionId": "strict-converge-001",
+ "definitionId": "3d3412c458178ff4",
"name": "Fork-Decision-Converge-Strict",
"description": "Tests the strictest Converge mode that requires ALL inbound connections to execute",
"createdAt": "2025-10-20T00:00:00.000000+00:00",
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json
index 8bc40e337..2da7bbac3 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-converge.json
@@ -1,7 +1,7 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "b60374027329240a",
- "definitionId": "3d3412c458178fff",
+ "definitionId": "3d3412c458178ff1",
"name": "Fork-issues",
"description": "show case",
"createdAt": "2025-09-30T16:56:12.420213\u002B00:00",
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json
index ddb3db562..3cdf365e5 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-none.json
@@ -1,7 +1,7 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "eed88d95ad53e0f",
- "definitionId": "3d3412c458178fff",
+ "definitionId": "3d3412c458178ff2",
"name": "Fork-issues",
"description": "show case",
"createdAt": "2025-09-30T17:03:24.623714\u002B00:00",
diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json
index a10bcfcd7..1e03e3247 100644
--- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json
+++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/JoinBehaviors/Workflows/fork-decision-join-waitall.json
@@ -1,7 +1,7 @@
{
"$schema": "https://elsaworkflows.io/schemas/workflow-definition/v3.0.0/schema.json",
"id": "93277d4cd53f2dfc",
- "definitionId": "3d3412c458178fff",
+ "definitionId": "3d3412c458178ff3",
"name": "Fork-issues",
"description": "show case",
"createdAt": "2025-09-30T17:19:02.380015\u002B00:00",
diff --git a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
index 0ab8e6d86..fbe18f8de 100644
--- a/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
+++ b/test/unit/Elsa.Activities.UnitTests/Branching/FlowJoinTests.cs
@@ -11,33 +11,33 @@ namespace Elsa.Activities.UnitTests.Branching;
public class FlowJoinTests
{
[Theory]
- [InlineData(true, FlowJoinMode.WaitAny)]
- [InlineData(true, FlowJoinMode.WaitAll)]
- [InlineData(false, FlowJoinMode.WaitAny)]
- [InlineData(false, FlowJoinMode.WaitAll)]
- public async Task Should_Complete_In_All_Flow_Mode_Combinations(bool useTokenFlow, FlowJoinMode joinMode)
+ [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAny)]
+ [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAll)]
+ [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAny)]
+ [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAll)]
+ public async Task Should_Complete_In_All_Flow_Mode_Combinations(FlowchartExecutionMode executionMode, FlowJoinMode joinMode)
{
// Arrange & Act
- var context = await ExecuteWithFlowModeAsync(useTokenFlow, joinMode);
+ var context = await ExecuteWithFlowModeAsync(executionMode, joinMode);
// Assert
Assert.True(context.IsCompleted);
}
[Theory]
- [InlineData(true, FlowJoinMode.WaitAny)]
- [InlineData(true, FlowJoinMode.WaitAll)]
- [InlineData(false, FlowJoinMode.WaitAny)]
- [InlineData(false, FlowJoinMode.WaitAll)]
- public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(bool useTokenFlow, FlowJoinMode joinMode)
+ [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAny)]
+ [InlineData(FlowchartExecutionMode.TokenBased, FlowJoinMode.WaitAll)]
+ [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAny)]
+ [InlineData(FlowchartExecutionMode.CounterBased, FlowJoinMode.WaitAll)]
+ public async Task Should_Execute_In_Flowchart_Context_For_All_Combinations(FlowchartExecutionMode executionMode, FlowJoinMode joinMode)
{
// Arrange & Act
- var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(useTokenFlow, joinMode);
+ var (context, flowJoin) = await ExecuteInFlowchartWithFlowModeAsync(executionMode, joinMode);
// Assert
Assert.NotNull(context);
- var expectedMessage = useTokenFlow
+ var expectedMessage = executionMode == FlowchartExecutionMode.TokenBased
? $"Token flow mode with {joinMode} should schedule the join activity"
: $"Counter flow mode with {joinMode} should schedule the join activity as start";
@@ -51,8 +51,8 @@ public class FlowJoinTests
var joinMode = FlowJoinMode.WaitAll; // Use WaitAll to highlight differences
// Act
- var tokenContext = await ExecuteWithFlowModeAsync(true, joinMode);
- var counterContext = await ExecuteWithFlowModeAsync(false, joinMode);
+ var tokenContext = await ExecuteWithFlowModeAsync(FlowchartExecutionMode.TokenBased, joinMode);
+ var counterContext = await ExecuteWithFlowModeAsync(FlowchartExecutionMode.CounterBased, joinMode);
// Assert
Assert.True(tokenContext.IsCompleted, "Token flow should always complete");
@@ -74,62 +74,48 @@ public class FlowJoinTests
};
///
- /// Executes a FlowJoin activity with the specified flow mode, handling the UseTokenFlow setup and teardown.
+ /// Executes a FlowJoin activity with the specified flow mode.
///
- private static async Task ExecuteWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode)
+ private static async Task ExecuteWithFlowModeAsync(FlowchartExecutionMode executionMode, FlowJoinMode joinMode)
{
- return await WithFlowModeAsync(useTokenFlow, async () =>
- {
- var flowJoin = CreateFlowJoin(joinMode);
- return await ExecuteAsync(flowJoin);
- });
+ var flowJoin = CreateFlowJoin(joinMode);
+ return await ExecuteAsync(flowJoin, executionMode);
}
///
/// Executes a FlowJoin activity within a flowchart context with the specified flow mode.
///
- private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(bool useTokenFlow, FlowJoinMode joinMode)
+ private static async Task<(ActivityExecutionContext context, FlowJoin flowJoin)> ExecuteInFlowchartWithFlowModeAsync(FlowchartExecutionMode executionMode, FlowJoinMode joinMode)
{
- return await WithFlowModeAsync(useTokenFlow, async () =>
- {
- var flowJoin = CreateFlowJoin(joinMode);
- var flowchart = CreateSimpleFlowchart(flowJoin);
- var context = await ExecuteFlowchartAsync(flowchart);
- return (context, flowJoin);
- });
+ var flowJoin = CreateFlowJoin(joinMode);
+ var flowchart = CreateSimpleFlowchart(flowJoin);
+ var context = await ExecuteFlowchartAsync(flowchart, executionMode);
+ return (context, flowJoin);
}
///
- /// Executes an action with the specified flow mode, ensuring proper setup and teardown of UseTokenFlow.
+ /// Executes a flowchart using the ActivityTestFixture with the specified execution mode.
///
- private static async Task WithFlowModeAsync(bool useTokenFlow, Func> action)
+ private static Task ExecuteFlowchartAsync(Flowchart flowchart, FlowchartExecutionMode? executionMode = null)
{
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = useTokenFlow;
+ return ExecuteAsync(flowchart, executionMode);
+ }
- try
+ ///
+ /// Executes an activity using the ActivityTestFixture with the specified execution mode.
+ ///
+ private static async Task ExecuteAsync(IActivity activity, FlowchartExecutionMode? executionMode = null)
+ {
+ var fixture = new ActivityTestFixture(activity);
+
+ if (executionMode.HasValue)
{
- return await action();
+ fixture.ConfigureContext(context =>
+ {
+ context.WorkflowExecutionContext.Properties[Flowchart.ExecutionModePropertyKey] = executionMode.Value;
+ });
}
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
- }
- ///
- /// Executes an activity using the ActivityTestFixture.
- ///
- private static async Task ExecuteAsync(IActivity activity)
- {
- return await new ActivityTestFixture(activity).ExecuteAsync();
- }
-
- ///
- /// Executes a flowchart using the ActivityTestFixture.
- ///
- private static async Task ExecuteFlowchartAsync(Flowchart flowchart)
- {
- return await new ActivityTestFixture(flowchart).ExecuteAsync();
+ return await fixture.ExecuteAsync();
}
}
diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs
index c504f48b7..b6c97a0c0 100644
--- a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs
+++ b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTestHelpers.cs
@@ -1,6 +1,7 @@
using Elsa.Testing.Shared;
using Elsa.Workflows;
using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Models;
namespace Elsa.Activities.UnitTests.Flow;
@@ -9,9 +10,18 @@ namespace Elsa.Activities.UnitTests.Flow;
///
public static class FlowchartTestHelpers
{
- public static async Task ExecuteFlowchartAsync(Flowchart flowchart)
+ public static async Task ExecuteFlowchartAsync(Flowchart flowchart, FlowchartExecutionMode? executionMode = null)
{
var fixture = new ActivityTestFixture(flowchart);
+
+ if (executionMode.HasValue)
+ {
+ fixture.ConfigureContext(context =>
+ {
+ context.WorkflowExecutionContext.Properties[Flowchart.ExecutionModePropertyKey] = executionMode.Value;
+ });
+ }
+
return await fixture.ExecuteAsync();
}
}
diff --git a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs
index cbca233e9..7396d1733 100644
--- a/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs
+++ b/test/unit/Elsa.Activities.UnitTests/Flow/FlowchartTests.cs
@@ -1,5 +1,6 @@
using Elsa.Testing.Shared;
using Elsa.Workflows.Activities.Flowchart.Activities;
+using Elsa.Workflows.Activities.Flowchart.Models;
using static Elsa.Activities.UnitTests.Flow.FlowchartTestHelpers;
namespace Elsa.Activities.UnitTests.Flow;
@@ -43,34 +44,24 @@ public class FlowchartTests
Assert.False(context.HasScheduledActivity(new WriteLine("NonExistent")));
}
- [Theory(DisplayName = "Respects UseTokenFlow flag")]
- [InlineData(true)]
- [InlineData(false)]
- public async Task RespectsUseTokenFlowFlag(bool useTokenFlow)
+ [Theory(DisplayName = "Respects execution mode")]
+ [InlineData(FlowchartExecutionMode.TokenBased)]
+ [InlineData(FlowchartExecutionMode.CounterBased)]
+ public async Task RespectsExecutionMode(FlowchartExecutionMode executionMode)
{
// Arrange
- var originalValue = Flowchart.UseTokenFlow;
- Flowchart.UseTokenFlow = useTokenFlow;
-
- try
+ var activity = new WriteLine("Test");
+ var flowchart = new Flowchart
{
- var activity = new WriteLine("Test");
- var flowchart = new Flowchart
- {
- Start = activity,
- Activities = { activity }
- };
+ Start = activity,
+ Activities = { activity }
+ };
- // Act
- var context = await ExecuteFlowchartAsync(flowchart);
+ // Act
+ var context = await ExecuteFlowchartAsync(flowchart, executionMode);
- // Assert - just verify it executes without error
- Assert.NotNull(context);
- }
- finally
- {
- Flowchart.UseTokenFlow = originalValue;
- }
+ // Assert - just verify it executes without error
+ Assert.NotNull(context);
}
[Fact(DisplayName = "Accepts empty connections collection")]