From e29e6efc838d1c62c26d1d2b795d8dc338762a62 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 12 Feb 2026 19:57:13 +0100 Subject: [PATCH 1/2] Add integration tests for enumerables with projection to arrays Introduce integration tests to verify the conversion of a Select-projected IEnumerable to an array using JavaScript execution. This includes creating new test cases, activities, and workflows to ensure the end-to-end process behaves as expected. Also refined type inference in `ExpressionExecutionContextExtensions` for projection operators. --- .../ExpressionExecutionContextExtensions.cs | 4 +- .../EnumerableProjectionTestWorkflow.cs | 38 +++++++++++++++++++ .../EnumerableProjectionTests.cs | 38 +++++++++++++++++++ .../TestEnumerableActivity.cs | 27 +++++++++++++ .../TestOutputItem.cs | 7 ++++ ...pressionExecutionContextExtensionsTests.cs | 9 +++-- 6 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTestWorkflow.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestEnumerableActivity.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestOutputItem.cs rename test/unit/Elsa.Workflows.Core.UnitTests/{ => Extensions/ExpressionExecutionContextExtensions}/ExpressionExecutionContextExtensionsTests.cs (87%) diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index b0c8964101..700f2d40b4 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -556,7 +556,9 @@ private static object ConvertIEnumerableToArray(object? obj) return obj; // Use LINQ to convert the IEnumerable to an array. - var elementType = obj.GetType().GetGenericArguments().FirstOrDefault(); + // For projection operators like Select, the element type is the LAST generic argument + // (e.g., ListSelectIterator where TResult is the element type) + var elementType = obj.GetType().GetGenericArguments().LastOrDefault(); if (elementType == null) return obj; diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTestWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTestWorkflow.cs new file mode 100644 index 0000000000..d127b1524c --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTestWorkflow.cs @@ -0,0 +1,38 @@ +using Elsa.Expressions.JavaScript.Activities; +using Elsa.Extensions; +using Elsa.Workflows.Activities; +using Elsa.Workflows.Memory; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray; + +/// +/// Code-first workflow that reproduces the "projected IEnumerable to array" scenario end-to-end. +/// +public class EnumerableProjectionTestWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder workflow) + { + // Store in workflow instance state to mimic real execution behavior. + var messagesVar = new Variable("Messages", Array.Empty()).WithWorkflowStorage(); + + workflow.WithVariables(messagesVar); + + workflow.Root = new Sequence + { + Activities = + { + new TestEnumerableActivity + { + EnumerableResult = new(messagesVar) + }, + + new RunJavaScript + { + // Accessing Messages through JS triggers the conversion logic. + Script = new("return getMessages();"), + Result = new(messagesVar) + } + } + }; + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs new file mode 100644 index 0000000000..3fa28a656e --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs @@ -0,0 +1,38 @@ +using Elsa.Expressions.Helpers; +using Elsa.Testing.Shared; +using Elsa.Workflows.Management; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray; + +public class EnumerableProjectionTests(ITestOutputHelper testOutputHelper) +{ + private readonly WorkflowTestFixture _fixture = new(testOutputHelper); + + [Fact(DisplayName = "JavaScript should convert a Select-projected IEnumerable variable to an array")] + public async Task Should_Convert_Select_Projected_Enumerable_To_Array() + { + // Arrange + var workflow = new EnumerableProjectionTestWorkflow(); + + // Act + var result = await _fixture.RunWorkflowAsync(workflow); + + // Assert + Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); // If conversion failed, workflow will have faulted. + var variableManager = _fixture.Services.GetRequiredService(); + + var messagesVariable = (await variableManager.GetVariablesAsync(result.WorkflowExecutionContext)).FirstOrDefault(x => x.Variable.Name == "Messages"); + var messages = messagesVariable?.Value.ConvertTo(); + + Assert.NotNull(messages); + Assert.Equal(5, messages.Length); + + Assert.All(messages, message => + { + Assert.Contains("Name:", message); + Assert.Contains("ID:", message); + }); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestEnumerableActivity.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestEnumerableActivity.cs new file mode 100644 index 0000000000..4f48618e9e --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestEnumerableActivity.cs @@ -0,0 +1,27 @@ +using Elsa.Workflows.Models; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray; + +/// +/// A test activity that produces an IEnumerable<string> via Select projection, which has two generic arguments and was triggering the bug in ConvertIEnumerableToArray. +/// +public class TestEnumerableActivity : Activity +{ + public Output EnumerableResult { get; set; } = new(); + + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) + { + // Create a list of items + var items = Enumerable + .Range(1, 5) + .Select(x => new TestOutputItem()) + .ToList(); + + // Use Select to project to strings - this creates a ListSelectIterator + // which has TWO generic arguments, triggering the bug + var messages = items.Select(e => $"Name: {e.Name} ID: {e.Id}"); + + context.Set(EnumerableResult, messages); + return context.CompleteActivityWithOutcomesAsync("Done"); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestOutputItem.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestOutputItem.cs new file mode 100644 index 0000000000..fd8513a3eb --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/TestOutputItem.cs @@ -0,0 +1,7 @@ +namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray; + +public class TestOutputItem +{ + public string Name { get; } = Guid.NewGuid().ToString(); + public string Id { get; } = Guid.NewGuid().ToString(); +} \ No newline at end of file diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/ExpressionExecutionContextExtensionsTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs similarity index 87% rename from test/unit/Elsa.Workflows.Core.UnitTests/ExpressionExecutionContextExtensionsTests.cs rename to test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs index fc881dab32..4afd76c17f 100644 --- a/test/unit/Elsa.Workflows.Core.UnitTests/ExpressionExecutionContextExtensionsTests.cs +++ b/test/unit/Elsa.Workflows.Core.UnitTests/Extensions/ExpressionExecutionContextExtensions/ExpressionExecutionContextExtensionsTests.cs @@ -1,5 +1,8 @@ +using Elsa.Expressions.JavaScript.Activities; using Elsa.Expressions.Models; using Elsa.Extensions; +using Elsa.Testing.Shared; +using Elsa.Workflows.Activities; using Elsa.Workflows.Memory; namespace Elsa.Workflows.Core.UnitTests; @@ -13,7 +16,7 @@ public void GetVariable_ReturnsVariable_WhenVariableExists() var variable = new Variable("test", 5); var memoryRegister = new MemoryRegister(new Dictionary { - { variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } + { variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } }); var context = new ExpressionExecutionContext(null!, memoryRegister); @@ -46,7 +49,7 @@ public void CreateVariable_ThrowsException_WhenVariableExists() var variable = new Variable("test", 5); var memoryRegister = new MemoryRegister(new Dictionary { - { variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } + { variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } }); var context = new ExpressionExecutionContext(null!, memoryRegister); @@ -92,7 +95,7 @@ public void SetVariable_SetsValue_WhenVariableExists() var variable = new Variable("test", 5); var memoryRegister = new MemoryRegister(new Dictionary { - { variable.Id, new MemoryBlock(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } + { variable.Id, new(variable.Value, new VariableBlockMetadata(variable, typeof(object), true)) } }); var context = new ExpressionExecutionContext(null!, memoryRegister); From 4b8043fb2dfc1706e92336d5af666e81d878c557 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 12 Feb 2026 19:59:54 +0100 Subject: [PATCH 2/2] Remove unused newline in EnumerableProjectionTests --- .../ProjectedEnumerableToArray/EnumerableProjectionTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs index 3fa28a656e..0f5eb3809e 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/ProjectedEnumerableToArray/EnumerableProjectionTests.cs @@ -22,7 +22,6 @@ public async Task Should_Convert_Select_Projected_Enumerable_To_Array() // Assert Assert.Equal(WorkflowSubStatus.Finished, result.WorkflowState.SubStatus); // If conversion failed, workflow will have faulted. var variableManager = _fixture.Services.GetRequiredService(); - var messagesVariable = (await variableManager.GetVariablesAsync(result.WorkflowExecutionContext)).FirstOrDefault(x => x.Variable.Name == "Messages"); var messages = messagesVariable?.Value.ConvertTo();