diff --git a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs index fd5608325..2076e2f8f 100644 --- a/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Builders/WorkflowBuilder.cs @@ -93,6 +93,7 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer { var variable = new Variable(name, value); Variables.Add(variable); + variable.WithWorkflowStorage(); return variable; } diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs index 3d7c3cb8a..3b6cea396 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowBuilder.cs @@ -116,7 +116,8 @@ public interface IWorkflowBuilder Variable WithVariable(); /// - /// A fluent method for adding a variable to . + /// A fluent method for adding a named variable to . + /// The variable uses workflow instance storage by default so its value survives suspend and resume. /// Variable WithVariable(string name, T value); diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Tests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Tests.cs new file mode 100644 index 000000000..4eec53b1c --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Tests.cs @@ -0,0 +1,47 @@ +using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Options; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.NamedVariablePersistence; + +public class Tests +{ + private readonly IWorkflowRunner _workflowRunner; + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IWorkflowBuilderFactory _workflowBuilderFactory; + private readonly IServiceProvider _services; + + public Tests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); + _workflowBuilderFactory = _services.GetRequiredService(); + _workflowRunner = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Named WithVariable value survives suspend and resume")] + public async Task NamedWithVariable_ValueSurvivesSuspendAndResume() + { + // Arrange + await _services.PopulateRegistriesAsync(); + var workflow = await _workflowBuilderFactory.CreateBuilder().BuildWorkflowAsync(); + + // Act + var started = await _workflowRunner.RunAsync(workflow); + var bookmark = started.WorkflowState.Bookmarks.Single(x => x.ActivityId == "Resume"); + var runOptions = new RunWorkflowOptions { BookmarkId = bookmark.Id }; + var resumed = await _workflowRunner.RunAsync(workflow, started.WorkflowState, runOptions); + + // Assert + Assert.Equal(WorkflowStatus.Running, started.WorkflowState.Status); + Assert.Equal(WorkflowSubStatus.Suspended, started.WorkflowState.SubStatus); + Assert.Equal(WorkflowStatus.Finished, resumed.WorkflowState.Status); + Assert.Equal( + [ + "before suspend: hello", + "after resume: hello" + ], + _capturingTextWriter.Lines.ToList()); + } +} diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Workflows.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Workflows.cs new file mode 100644 index 000000000..bec5fd97f --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/NamedVariablePersistence/Workflows.cs @@ -0,0 +1,23 @@ +using Elsa.Workflows.Activities; +using Elsa.Workflows.Runtime.Activities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.NamedVariablePersistence; + +class NamedVariableSurvivesSuspendWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + var message = builder.WithVariable("message", null!); + + builder.Root = new Sequence + { + Activities = + { + new SetVariable(message, "hello"), + new WriteLine(context => $"before suspend: {message.Get(context) ?? ""}"), + new Event("Resume") { Id = "Resume" }, + new WriteLine(context => $"after resume: {message.Get(context) ?? ""}") + } + }; + } +} diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Builders/WorkflowBuilderTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Builders/WorkflowBuilderTests.cs new file mode 100644 index 000000000..ab4c9de9a --- /dev/null +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Builders/WorkflowBuilderTests.cs @@ -0,0 +1,62 @@ +using Elsa.Workflows.Builders; +using Elsa.Workflows.Memory; +using NSubstitute; + +namespace Elsa.Workflows.Core.UnitTests.Builders; + +public class WorkflowBuilderTests +{ + [Fact] + public void WithVariable_NameAndValue_UsesWorkflowInstanceStorage() + { + // Arrange + var builder = CreateBuilder(); + + // Act + var variable = builder.WithVariable("message", "hello"); + + // Assert + Assert.Equal("message", variable.Name); + Assert.Equal("hello", variable.Value); + Assert.Equal(typeof(WorkflowInstanceStorageDriver), variable.StorageDriverType); + Assert.Contains(variable, builder.Variables); + } + + [Fact] + public void WithVariable_NameAndValue_UsesSameStorageAsParameterlessOverload() + { + // Arrange + var builder = CreateBuilder(); + + // Act +#pragma warning disable CS0618 // Parameterless overload is obsolete but remains the persistence baseline. + var unnamed = builder.WithVariable(); +#pragma warning restore CS0618 + var named = builder.WithVariable("message", "hello"); + + // Assert + Assert.Equal(typeof(WorkflowInstanceStorageDriver), unnamed.StorageDriverType); + Assert.Equal(unnamed.StorageDriverType, named.StorageDriverType); + } + + [Fact] + public void WithVariable_ExistingVariable_DoesNotOverrideStorageDriver() + { + // Arrange + var builder = CreateBuilder(); + var variable = new Variable("message", "hello"); + + // Act + builder.WithVariable(variable); + + // Assert + Assert.Null(variable.StorageDriverType); + Assert.Contains(variable, builder.Variables); + } + + private static WorkflowBuilder CreateBuilder() => + new( + Substitute.For(), + Substitute.For(), + Substitute.For()); +}