Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a99a6c3
Introduce `FlowchartExecutionMode` to streamline flowchart execution …
sfmskywalker Dec 11, 2025
9ed5cde
Add support for configuring flowchart execution behavior via `Flowcha…
sfmskywalker Dec 11, 2025
9dc2129
Update default Flowchart execution settings to align with version 3.5…
sfmskywalker Dec 11, 2025
6ef97bd
Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/Fl…
sfmskywalker Dec 11, 2025
5d4afa2
Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extension…
sfmskywalker Dec 11, 2025
061476d
Update test/integration/Elsa.Workflows.IntegrationTests/Scenarios/Flo…
sfmskywalker Dec 11, 2025
713d48f
Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Extension…
sfmskywalker Dec 11, 2025
7732163
Refactor flowchart integration tests for improved formatting and cons…
sfmskywalker Dec 11, 2025
3c4596f
Refactor workflow tests and related services to improve reusability a…
sfmskywalker Dec 11, 2025
24203e2
Merge remote-tracking branch 'origin/enh/flowchart-token-toggle' into…
sfmskywalker Dec 11, 2025
8cb3368
Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Options/F…
sfmskywalker Dec 11, 2025
9b53ce0
Refactor flowchart execution logic to use `FlowchartExecutionMode` en…
sfmskywalker Dec 11, 2025
25be18f
Merge remote-tracking branch 'origin/enh/flowchart-token-toggle' into…
sfmskywalker Dec 11, 2025
87aab09
Refactor tests and workflow logic to replace boolean `useTokenFlow` w…
sfmskywalker Dec 11, 2025
971b40a
Refactor flowchart execution logic to centralize mode-based behavior …
sfmskywalker Dec 11, 2025
58c610b
Remove unnecessary whitespace in Flowchart.cs to improve code formatting
sfmskywalker Dec 11, 2025
ad3cef6
Remove unnecessary whitespace in FlowJoinTests.cs to improve code for…
sfmskywalker Dec 11, 2025
b9aaab8
Update src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
sfmskywalker Dec 12, 2025
ab9f637
Update src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activitie…
sfmskywalker Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/apps/Elsa.Server.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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;
Expand Down Expand Up @@ -60,6 +61,7 @@
strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
});
})
.UseFlowchart(flowchart => flowchart.UseTokenBasedExecution())
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef => ef.UseSqlite());
Expand Down
66 changes: 66 additions & 0 deletions src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Elsa.Workflows.Activities;
using Elsa.Workflows.Memory;
using Elsa.Workflows.Models;
using Elsa.Workflows.Options;
using Elsa.Workflows.State;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -122,6 +123,54 @@ public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, Cancel
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync(workflow, cancellationToken: cancellationToken);
}

/// <summary>
/// Runs the specified workflow and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunWorkflowAsync<TWorkflow>(CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
{
if (_services == null)
await BuildAsync();

var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync<TWorkflow>(cancellationToken: cancellationToken);
}

/// <summary>
/// Runs a workflow with the specified options and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="workflow">The workflow to run</param>
/// <param name="options">Workflow execution options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, RunWorkflowOptions options, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();

var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync(workflow, options, cancellationToken);
}

/// <summary>
/// Runs the specified workflow with the specified options and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="options">Workflow execution options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunWorkflowAsync<TWorkflow>(RunWorkflowOptions options, CancellationToken cancellationToken = default) where TWorkflow : IWorkflow, new()
{
if (_services == null)
await BuildAsync();

var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync<TWorkflow>(options, cancellationToken);
}

/// <summary>
/// Runs an activity wrapped in a workflow and returns the workflow result.
Expand All @@ -139,6 +188,23 @@ public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, Cancel
return await workflowRunner.RunAsync(activity, cancellationToken: cancellationToken);
}

/// <summary>
/// Runs an activity wrapped in a workflow with the specified options and returns the workflow result.
/// Automatically builds the fixture if not already built.
/// </summary>
/// <param name="activity">The activity to run</param>
/// <param name="options">Workflow execution options</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, RunWorkflowOptions options, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();

var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
return await workflowRunner.RunAsync(activity, options, cancellationToken);
}

/// <summary>
/// Runs a workflow by definition ID and returns the workflow state.
/// Automatically builds the fixture if not already built.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,10 +16,17 @@ namespace Elsa.Workflows.Activities.Flowchart.Activities;
[Browsable(false)]
public partial class Flowchart : Container
{
/// <summary>
/// The property key used to store the flowchart execution mode in <see cref="WorkflowExecutionContext.Properties"/>.
/// </summary>
public const string ExecutionModePropertyKey = "Flowchart:ExecutionMode";

/// <summary>
/// Set this to <c>false</c> 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 <see cref="FlowchartOptions"/> configured via DI for application-wide settings.
/// </summary>
public static bool UseTokenFlow = true;
public static bool UseTokenFlow = false; // Default to false in order to maintain the same behavior with 3.5.2 out of the box.

/// <inheritdoc />
public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
Expand All @@ -29,7 +39,7 @@ public Flowchart([CallerFilePath] string? source = null, [CallerLineNumber] int?
/// <summary>
/// The activity to execute when the flowchart starts.
/// </summary>
[Port][Browsable(false)] public IActivity? Start { get; set; }
[Port] [Browsable(false)] public IActivity? Start { get; set; }

/// <summary>
/// A list of connections between activities.
Expand Down Expand Up @@ -78,18 +88,66 @@ private async ValueTask OnScheduleChildActivityAsync(ScheduleChildActivity signa

private ValueTask OnChildCompletedAsync(ActivityCompletedContext context)
{
return UseTokenFlow
return GetEffectiveExecutionMode(context.TargetContext)
? OnChildCompletedTokenBasedLogicAsync(context)
: OnChildCompletedCounterBasedLogicAsync(context);
}

private ValueTask OnActivityCanceledAsync(CancelSignal signal, SignalContext context)
{
return UseTokenFlow
return GetEffectiveExecutionMode(context.ReceiverActivityExecutionContext)
? OnTokenFlowActivityCanceledAsync(signal, context)
: OnCounterFlowActivityCanceledAsync(signal, context);
}

/// <summary>
/// Gets the effective execution mode for this flowchart execution.
/// Returns true for token-based mode, false for counter-based mode.
/// Priority: WorkflowExecutionContext.Properties > FlowchartOptions (DI) > Static UseTokenFlow flag
/// </summary>
private bool GetEffectiveExecutionMode(ActivityExecutionContext context)
{
var workflowExecutionContext = context.WorkflowExecutionContext;

if (workflowExecutionContext.Properties.TryGetValue(ExecutionModePropertyKey, out var modeValue))
{
var mode = ParseExecutionMode(modeValue);

if (mode != FlowchartExecutionMode.Default)
return ConvertModeToBoolean(mode);
}

var defaultMode = GetDefaultModeFromOptionsAsEnum(context);
return ConvertModeToBoolean(defaultMode);
}
Comment thread
sfmskywalker marked this conversation as resolved.
Outdated

private FlowchartExecutionMode ParseExecutionMode(object modeValue)
{
return modeValue switch
{
FlowchartExecutionMode executionMode => executionMode,
string str when Enum.TryParse<FlowchartExecutionMode>(str, true, out var parsed) => parsed,
int intValue when Enum.IsDefined(typeof(FlowchartExecutionMode), intValue) => (FlowchartExecutionMode)intValue,
_ => FlowchartExecutionMode.Default
};
}

private bool ConvertModeToBoolean(FlowchartExecutionMode mode)
{
return mode switch
{
FlowchartExecutionMode.TokenBased => true,
FlowchartExecutionMode.CounterBased => false,
_ => UseTokenFlow
};
}

private FlowchartExecutionMode GetDefaultModeFromOptionsAsEnum(ActivityExecutionContext context)
{
var options = context.WorkflowExecutionContext.ServiceProvider.GetService<IOptions<FlowchartOptions>>();
return options?.Value.DefaultExecutionMode ?? FlowchartExecutionMode.Default;
Comment thread
sfmskywalker marked this conversation as resolved.
Outdated
}

private async Task CompleteIfNoPendingWorkAsync(ActivityExecutionContext context)
{
var hasPendingWork = HasPendingWork(context);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for <see cref="FlowchartFeature"/>.
/// </summary>
public static class FlowchartFeatureExtensions
{
extension(FlowchartFeature feature)
{
/// <summary>
/// Configures the flowchart options.
/// </summary>
public FlowchartFeature ConfigureFlowchart(Action<FlowchartOptions> configure)
{
feature.FlowchartOptionsConfigurator = configure;
return feature;
}

/// <summary>
/// Sets the default execution mode for flowcharts to token-based.
/// </summary>
public FlowchartFeature UseTokenBasedExecution()
{
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.TokenBased);
}

/// <summary>
/// Sets the default execution mode for flowcharts to counter-based (legacy mode).
/// </summary>
public FlowchartFeature UseCounterBasedExecution()
{
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = FlowchartExecutionMode.CounterBased);
}

/// <summary>
/// Sets the default execution mode for flowcharts to the specified mode.
/// </summary>
Comment thread
sfmskywalker marked this conversation as resolved.
public FlowchartFeature UseExecution(FlowchartExecutionMode mode)
{
return feature.ConfigureFlowchart(options => options.DefaultExecutionMode = mode);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Options;

namespace Elsa.Workflows.Activities.Flowchart.Extensions;

/// <summary>
/// Extension methods for <see cref="RunWorkflowOptions"/> to configure flowchart execution mode.
/// </summary>
public static class RunWorkflowOptionsExtensions
{
extension(RunWorkflowOptions options)
{
/// <summary>
/// Sets the flowchart execution mode to token-based.
/// </summary>
public RunWorkflowOptions WithTokenBasedFlowchart()
{
return options.WithFlowchartExecutionMode(FlowchartExecutionMode.TokenBased);
}

/// <summary>
/// Sets the flowchart execution mode to counter-based (legacy mode).
/// </summary>
public RunWorkflowOptions WithCounterBasedFlowchart()
{
return options.WithFlowchartExecutionMode(FlowchartExecutionMode.CounterBased);
}

/// <summary>
/// Sets the flowchart execution mode.
/// </summary>
public RunWorkflowOptions WithFlowchartExecutionMode(FlowchartExecutionMode mode)
{
options.Properties ??= new Dictionary<string, object>();
options.Properties[Elsa.Workflows.Activities.Flowchart.Activities.Flowchart.ExecutionModePropertyKey] = mode;
return options;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Elsa.Workflows.Activities.Flowchart.Models;

/// <summary>
/// Specifies the execution mode for flowchart activities.
/// </summary>
public enum FlowchartExecutionMode
{
/// <summary>
/// Use the default mode as specified by <see cref="Elsa.Workflows.Activities.Flowchart.Flowchart.UseTokenFlow"/>.

Check warning on line 9 in src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

XML comment has cref attribute 'UseTokenFlow' that could not be resolved

Check warning on line 9 in src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

XML comment has cref attribute 'UseTokenFlow' that could not be resolved

Check warning on line 9 in src/modules/Elsa.Workflows.Core/Activities/Flowchart/Models/FlowchartExecutionMode.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

XML comment has cref attribute 'UseTokenFlow' that could not be resolved
/// </summary>
Comment thread
sfmskywalker marked this conversation as resolved.
Default = 0,

/// <summary>
/// Use token-based flow logic.
/// </summary>
TokenBased = 1,

/// <summary>
/// Use counter-based flow logic (legacy mode).
/// </summary>
CounterBased = 2
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Elsa.Workflows.Activities.Flowchart.Models;

namespace Elsa.Workflows.Activities.Flowchart.Options;

/// <summary>
/// Options for configuring flowchart execution behavior.
/// </summary>
public class FlowchartOptions
{
/// <summary>
/// Gets or sets the default execution mode for flowcharts when not explicitly specified.
/// Defaults to <see cref="FlowchartExecutionMode.TokenBased"/>.
Comment thread
sfmskywalker marked this conversation as resolved.
Outdated
/// </summary>
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.
}
15 changes: 12 additions & 3 deletions src/modules/Elsa.Workflows.Core/Extensions/ModuleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,18 @@ namespace Elsa.Extensions;

public static class ModuleExtensions
{
public static IModule UseWorkflows(this IModule configuration, Action<WorkflowsFeature>? configure = default)
extension(IModule configuration)
{
configuration.Configure(configure);
return configuration;
public IModule UseWorkflows(Action<WorkflowsFeature>? configure = null)
{
configuration.Configure(configure);
return configuration;
}

public IModule UseFlowchart(Action<FlowchartFeature>? configure = null)
{
configuration.Configure(configure);
return configuration;
}
}
}
14 changes: 13 additions & 1 deletion src/modules/Elsa.Workflows.Core/Features/FlowchartFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
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;

Expand All @@ -16,11 +18,21 @@ public FlowchartFeature(IModule module) : base(module)
{
}

/// <summary>
/// A delegate to configure <see cref="FlowchartOptions"/>.
/// </summary>
public Action<FlowchartOptions>? FlowchartOptionsConfigurator { get; set; }

/// <inheritdoc />
public override void Apply()
{
Services.AddSerializationOptionsConfigurator<FlowchartSerializationOptionConfigurator>();


// Register FlowchartOptions
Services.AddOptions<FlowchartOptions>();

if (FlowchartOptionsConfigurator != null)
Services.Configure(FlowchartOptionsConfigurator);
}

public override void Configure()
Expand Down
Loading
Loading