Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@
var activity = activityWithOutput.Activity;
var activityDescriptor = activityWithOutput.ActivityDescriptor;
var activityIdentifier = useActivityName ? activity.Name : activity.Id;
var activityIdPascalName = activityIdentifier.Pascalize();

Check warning on line 476 in src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

Possible null reference argument for parameter 'input' in 'string InflectorExtensions.Pascalize(string input)'.

Check warning on line 476 in src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

Possible null reference argument for parameter 'input' in 'string InflectorExtensions.Pascalize(string input)'.

Check warning on line 476 in src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs

View workflow job for this annotation

GitHub Actions / Test with coverage

Possible null reference argument for parameter 'input' in 'string InflectorExtensions.Pascalize(string input)'.

foreach (var output in activityDescriptor.Outputs)
{
Expand Down Expand Up @@ -556,7 +556,9 @@
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<TSource, TResult> where TResult is the element type)
var elementType = obj.GetType().GetGenericArguments().LastOrDefault();

if (elementType == null)
return obj;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Code-first workflow that reproduces the "projected IEnumerable to array" scenario end-to-end.
/// </summary>
public class EnumerableProjectionTestWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder workflow)
{
// Store in workflow instance state to mimic real execution behavior.
var messagesVar = new Variable<object>("Messages", Array.Empty<string>()).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)
}
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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);

Comment thread
sfmskywalker marked this conversation as resolved.
[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<IWorkflowInstanceVariableManager>();
var messagesVariable = (await variableManager.GetVariablesAsync(result.WorkflowExecutionContext)).FirstOrDefault(x => x.Variable.Name == "Messages");
var messages = messagesVariable?.Value.ConvertTo<string[]>();

Assert.NotNull(messages);
Assert.Equal(5, messages.Length);

Assert.All(messages, message =>
{
Assert.Contains("Name:", message);
Assert.Contains("ID:", message);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Elsa.Workflows.Models;

namespace Elsa.Workflows.IntegrationTests.Scenarios.ProjectedEnumerableToArray;

/// <summary>
/// A test activity that produces an IEnumerable&lt;string&gt; via Select projection, which has two generic arguments and was triggering the bug in ConvertIEnumerableToArray.
/// </summary>
public class TestEnumerableActivity : Activity
{
public Output<object?> 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<TestOutputItem, string>
// 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");
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,7 +16,7 @@ public void GetVariable_ReturnsVariable_WhenVariableExists()
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ 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);
Expand Down Expand Up @@ -46,7 +49,7 @@ public void CreateVariable_ThrowsException_WhenVariableExists()
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ 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);
Expand Down Expand Up @@ -92,7 +95,7 @@ public void SetVariable_SetsValue_WhenVariableExists()
var variable = new Variable("test", 5);
var memoryRegister = new MemoryRegister(new Dictionary<string, MemoryBlock>
{
{ 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);
Expand Down
Loading