Skip to content
Open
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
17 changes: 16 additions & 1 deletion src/WorkflowCore.DSL/Services/DefinitionLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private void AttachInputs(StepSourceV1 source, Type dataType, Type stepType, Wor
var dataParameter = Expression.Parameter(dataType, "data");
var contextParameter = Expression.Parameter(typeof(IStepExecutionContext), "context");
var environmentVarsParameter = Expression.Parameter(typeof(IDictionary), "environment");
var stepProperty = stepType.GetProperty(input.Key);
var stepProperty = stepType.GetProperty(input.Key, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

if (stepProperty == null)
{
Expand All @@ -243,6 +243,21 @@ private void AttachInputs(StepSourceV1 source, Type dataType, Type stepType, Wor
continue;
}

// Handle primitive values (bool, int, etc.) directly
if (input.Value != null && (input.Value.GetType().IsPrimitive || input.Value is bool || input.Value is int || input.Value is long || input.Value is double || input.Value is decimal))
Copy link
Preview

Copilot AI Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition input.Value is bool || input.Value is int || input.Value is long || input.Value is double || input.Value is decimal is redundant since these types are already covered by input.Value.GetType().IsPrimitive. Consider simplifying to just check IsPrimitive or use a more explicit approach with a dedicated method.

Suggested change
if (input.Value != null && (input.Value.GetType().IsPrimitive || input.Value is bool || input.Value is int || input.Value is long || input.Value is double || input.Value is decimal))
if (input.Value != null && (input.Value.GetType().IsPrimitive || input.Value is decimal))

Copilot uses AI. Check for mistakes.

{
var primitiveValue = input.Value;
void primitiveAction(IStepBody pStep, object pData)
{
if (stepProperty.PropertyType.IsAssignableFrom(primitiveValue.GetType()))
stepProperty.SetValue(pStep, primitiveValue);
else
stepProperty.SetValue(pStep, System.Convert.ChangeType(primitiveValue, stepProperty.PropertyType));
Copy link
Preview

Copilot AI Oct 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The System.Convert.ChangeType call can throw InvalidCastException or FormatException for incompatible types, but these exceptions won't provide context about which property or step failed. Consider wrapping this in a try-catch block that provides a more descriptive error message including the property name and step ID.

Suggested change
stepProperty.SetValue(pStep, System.Convert.ChangeType(primitiveValue, stepProperty.PropertyType));
try
{
stepProperty.SetValue(pStep, System.Convert.ChangeType(primitiveValue, stepProperty.PropertyType));
}
catch (InvalidCastException ex)
{
throw new ArgumentException($"Failed to convert input '{input.Key}' for step '{source.Id}' to type '{stepProperty.PropertyType.Name}'.", ex);
}
catch (FormatException ex)
{
throw new ArgumentException($"Failed to convert input '{input.Key}' for step '{source.Id}' to type '{stepProperty.PropertyType.Name}'.", ex);
}

Copilot uses AI. Check for mistakes.

}
step.Inputs.Add(new ActionParameter<IStepBody, object>(primitiveAction));
continue;
}

throw new ArgumentException($"Unknown type for input {input.Key} on {source.Id}");
}
}
Expand Down
2 changes: 2 additions & 0 deletions test/WorkflowCore.TestAssets/DataTypes/CounterBoard.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;

namespace WorkflowCore.TestAssets.DataTypes
{
Expand All @@ -17,5 +18,6 @@ public class CounterBoard
public bool Flag1 { get; set; }
public bool Flag2 { get; set; }
public bool Flag3 { get; set; }
public List<string> DataList { get; set; } = new List<string> { "item1", "item2", "item3" };
}
}
12 changes: 12 additions & 0 deletions test/WorkflowCore.TestAssets/Steps/IterateListStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using WorkflowCore.Interface;
using WorkflowCore.Models;
using WorkflowCore.Primitives;

namespace WorkflowCore.TestAssets.Steps
{
public class IterateListStep : Foreach
{
// This class inherits from Foreach and should be able to use
// the RunParallel property from the base class in YAML definitions
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using FakeItEasy;
using WorkflowCore.Interface;
using WorkflowCore.Services.DefinitionStorage;
using Xunit;

namespace WorkflowCore.UnitTests.Services.DefinitionStorage
{
/// <summary>
/// Integration test to verify the fix for inherited property binding in YAML definitions.
/// This test specifically reproduces the issue mentioned in GitHub issue #1375.
/// </summary>
public class YamlInheritedPropertyIntegrationTest
{
[Fact(DisplayName = "Should bind inherited properties like RunParallel from base Foreach class")]
public void ShouldBindInheritedPropertiesInYamlDefinition()
{
// Arrange
var registry = A.Fake<IWorkflowRegistry>();
var loader = new DefinitionLoader(registry, new TypeResolver());

// This YAML definition uses a custom step (IterateListStep) that inherits from Foreach
// and tries to set the RunParallel property which is defined in the base Foreach class
var yamlWithInheritedProperty = @"
Id: TestInheritedPropertyWorkflow
Version: 1
Description: Test workflow for inherited property binding
DataType: WorkflowCore.TestAssets.DataTypes.CounterBoard, WorkflowCore.TestAssets
Steps:
- Id: IterateList
StepType: WorkflowCore.TestAssets.Steps.IterateListStep, WorkflowCore.TestAssets
Inputs:
Collection: ""data.DataList""
RunParallel: false
";

// Act & Assert
// Before the fix, this would throw: "Unknown property for input RunParallel on IterateList"
// After the fix, this should succeed
var exception = Record.Exception(() =>
loader.LoadDefinition(yamlWithInheritedProperty, Deserializers.Yaml));

// Verify no exception was thrown
Assert.Null(exception);
}

[Fact(DisplayName = "Should still throw exception for truly unknown properties")]
public void ShouldStillThrowForUnknownProperties()
{
// Arrange
var registry = A.Fake<IWorkflowRegistry>();
var loader = new DefinitionLoader(registry, new TypeResolver());

var yamlWithUnknownProperty = @"
Id: TestInheritedPropertyWorkflow
Version: 1
Description: Test workflow for inherited property binding
DataType: WorkflowCore.TestAssets.DataTypes.CounterBoard, WorkflowCore.TestAssets
Steps:
- Id: IterateList
StepType: WorkflowCore.TestAssets.Steps.IterateListStep, WorkflowCore.TestAssets
Inputs:
Collection: ""data.DataList""
NonExistentProperty: false
";

// Act & Assert
// This should still throw an exception for truly unknown properties
var exception = Assert.Throws<ArgumentException>(() =>
loader.LoadDefinition(yamlWithUnknownProperty, Deserializers.Yaml));

Assert.Contains("Unknown property for input NonExistentProperty", exception.Message);
}
}
}
Loading