Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.UseCounterBasedExecution())
.UseWorkflowManagement(management =>
{
management.UseEntityFrameworkCore(ef => ef.UseSqlite());
Expand Down
112 changes: 59 additions & 53 deletions src/common/Elsa.Testing.Shared.Integration/RunWorkflowExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,70 +20,76 @@ namespace Elsa.Testing.Shared;
[PublicAPI]
public static class RunWorkflowExtensions
{
/// <summary>
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="workflowDefinitionId">The ID of the workflow definition.</param>
/// <param name="input">An optional dictionary of input values.</param>
/// <param name="correlationId">An optional correlation id of the workflow.</param>
/// <param name="versionOptions">An optional set of options to specify the version of the workflow definition to retrieve.</param>
/// <returns>The workflow state.</returns>
public static async Task<WorkflowState> RunWorkflowUntilEndAsync(this IServiceProvider services,
string workflowDefinitionId,
IDictionary<string, object>? input = null,
string? correlationId = null,
VersionOptions? versionOptions = null)
extension(IServiceProvider services)
{
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
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<IWorkflowRuntime>();
var workflowClient = await workflowRuntime.CreateClientAsync();
var response = await workflowClient.CreateAndRunInstanceAsync(new()
/// <summary>
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
/// </summary>
/// <param name="workflowDefinitionId">The ID of the workflow definition.</param>
/// <param name="input">An optional dictionary of input values.</param>
/// <param name="correlationId">An optional correlation id of the workflow.</param>
/// <param name="versionOptions">An optional set of options to specify the version of the workflow definition to retrieve.</param>
/// <param name="runWorkflowOptions">Optional workflow execution options.</param>
/// <returns>The workflow state.</returns>
public async Task<WorkflowState> RunWorkflowUntilEndAsync(string workflowDefinitionId,
IDictionary<string, object>? 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<IBookmarkStore>();
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
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<IWorkflowRuntime>();
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<IBookmarkStore>();

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()
{
WorkflowInstanceId = response.WorkflowInstanceId
})).ToList();

if (!bookmarks.Any())
break;

foreach (var bookmark in bookmarks)
{
BookmarkId = bookmark.Id,
Input = input
};
response = await workflowClient.RunInstanceAsync(runRequest);
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();
}

/// <summary>
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
/// </summary>
public static async Task<WorkflowState> RunWorkflowUntilEndAsync<TWorkflow>(this IServiceProvider services, IDictionary<string, object>? input = null) where TWorkflow : IWorkflow
{
var workflowDefinitionId = typeof(TWorkflow).Name;
return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
/// <summary>
/// Runs a workflow until its end, automatically resuming any bookmark it encounters.
/// </summary>
public async Task<WorkflowState> RunWorkflowUntilEndAsync<TWorkflow>(IDictionary<string, object>? input = null) where TWorkflow : IWorkflow
{
var workflowDefinitionId = typeof(TWorkflow).Name;
return await services.RunWorkflowUntilEndAsync(workflowDefinitionId, input);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,51 +20,52 @@ namespace Elsa.Testing.Shared;
[PublicAPI]
public static class ServiceProviderExtensions
{
/// <summary>
/// Updates the registries.
/// </summary>
/// <param name="services">The services.</param>
public static Task PopulateRegistriesAsync(this IServiceProvider services)
extension(IServiceProvider services)
{
var registriesPopulator = services.GetRequiredService<IRegistriesPopulator>();
return registriesPopulator.PopulateAsync();
}

/// <summary>
/// Imports a workflow definition from a file.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="fileName">The file name.</param>
/// <returns>The workflow definition.</returns>
public static async Task<WorkflowDefinition> ImportWorkflowDefinitionAsync(this IServiceProvider services, string fileName)
{
var json = await File.ReadAllTextAsync(fileName);
var serializer = services.GetRequiredService<IActivitySerializer>();
var model = serializer.Deserialize<WorkflowDefinitionModel>(json);
/// <summary>
/// Updates the registries.
/// </summary>
public Task PopulateRegistriesAsync()
{
var registriesPopulator = services.GetRequiredService<IRegistriesPopulator>();
return registriesPopulator.PopulateAsync();
}

var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
/// <summary>
/// Imports a workflow definition from a file.
/// </summary>
/// <param name="fileName">The file name.</param>
/// <returns>The workflow definition.</returns>
public async Task<WorkflowDefinition> ImportWorkflowDefinitionAsync(string fileName)
{
Model = model,
Publish = true
};
var json = await File.ReadAllTextAsync(fileName);
var serializer = services.GetRequiredService<IActivitySerializer>();
var model = serializer.Deserialize<WorkflowDefinitionModel>(json);

var workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
return result.WorkflowDefinition;
}
var workflowDefinitionRequest = new SaveWorkflowDefinitionRequest
{
Model = model,
Publish = true
};

/// <summary>
/// Retrieves a workflow definition by its ID.
/// </summary>
/// <param name="services">The service provider.</param>
/// <param name="workflowDefinitionId">The definition ID of the workflow definition.</param>
/// <param name="versionOptions">Options to specify the version of the workflow definition to retrieve.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>The retrieved workflow definition.</returns>
public static async Task<WorkflowDefinition> GetWorkflowDefinitionAsync(this IServiceProvider services, string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
return workflowDefinition!;
var workflowDefinitionImporter = services.GetRequiredService<IWorkflowDefinitionImporter>();
var result = await workflowDefinitionImporter.ImportAsync(workflowDefinitionRequest);
return result.WorkflowDefinition;
}

/// <summary>
/// Retrieves a workflow definition by its ID.
/// </summary>
/// <param name="workflowDefinitionId">The definition ID of the workflow definition.</param>
/// <param name="versionOptions">Options to specify the version of the workflow definition to retrieve.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>The retrieved workflow definition.</returns>
public async Task<WorkflowDefinition> GetWorkflowDefinitionAsync(string workflowDefinitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var workflowDefinitionService = services.GetRequiredService<IWorkflowDefinitionService>();
var workflowDefinition = await workflowDefinitionService.FindWorkflowDefinitionAsync(workflowDefinitionId, versionOptions, cancellationToken);
return workflowDefinition!;
}
}
}
84 changes: 74 additions & 10 deletions src/common/Elsa.Testing.Shared.Integration/WorkflowTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
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;
Expand Down Expand Up @@ -102,6 +104,9 @@ public WorkflowTestFixture WithWorkflowsFromDirectory(params string[] directory)
/// </summary>
public async Task<WorkflowTestFixture> BuildAsync()
{
if (_services != null)
return this;

_services = _testApplicationBuilder.Build();
await Services.PopulateRegistriesAsync();
return this;
Expand All @@ -116,12 +121,52 @@ public async Task<WorkflowTestFixture> BuildAsync()
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();

await BuildAsync();
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()
{
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)
{
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()
{
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 @@ -132,26 +177,45 @@ public async Task<RunWorkflowResult> RunWorkflowAsync(IWorkflow workflow, Cancel
/// <returns>The workflow result after execution</returns>
public async Task<RunWorkflowResult> RunActivityAsync(IActivity activity, CancellationToken cancellationToken = default)
{
if (_services == null)
await BuildAsync();

await BuildAsync();
var workflowRunner = Services.GetRequiredService<IWorkflowRunner>();
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)
{
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.
/// </summary>
/// <param name="definitionId">The workflow definition ID</param>
/// <param name="input">Optional input dictionary</param>
/// <param name="options">Optional workflow execution options</param>
/// <returns>The workflow state after execution</returns>
public async Task<WorkflowState> RunWorkflowAsync(string definitionId, IDictionary<string, object>? input = null)
public async Task<WorkflowState> RunWorkflowAsync(string definitionId, IDictionary<string, object>? 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<WorkflowDefinition> ImportWorkflowDefinitionAsync(string fileName)
{
await BuildAsync();
return await Services.ImportWorkflowDefinitionAsync(fileName);
}

/// <summary>
Expand Down
Loading
Loading