.NET: Fix single-column value unwrap in declarative workflow (#6367)

* Fix single-column value unwrap in declarative workflow

* Added more tests
This commit is contained in:
Peter Ibekwe
2026-06-08 04:37:12 -07:00
committed by GitHub
Unverified
parent fa9e086576
commit 331201294b
2 changed files with 71 additions and 1 deletions
@@ -49,7 +49,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
EvaluationResult<DataValue> expressionResult = this.Evaluator.GetValue(this.Model.Items);
if (expressionResult.Value is TableDataValue tableValue)
{
this._values = [.. tableValue.Values.Select(value => value.ToFormula())];
this._values = [.. tableValue.Values.Select(ToLoopValue)];
}
else
{
@@ -99,6 +99,15 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor<Foreach>
}
}
// Power Fx wraps scalar array literals (`=[1, 2, 3]`) as `Table({Value: 1}, ...)`. Unwrap that single-column
// `Value`-record shape so `Local.LoopValue` is the scalar; multi-field and other shapes pass through unchanged.
private static FormulaValue ToLoopValue(DataValue value) =>
value is RecordDataValue record
&& record.Properties.Count == 1
&& record.Properties.TryGetValue("Value", out DataValue? singleColumn)
? singleColumn.ToFormula()
: value.ToFormula();
/// <inheritdoc/>
/// <remarks>
/// Persists the iteration cursor (<see cref="_index"/>), the materialized item snapshot
@@ -170,6 +170,67 @@ public sealed class ForeachExecutorTest(ITestOutputHelper output) : WorkflowActi
Assert.Equal("Engineer", currentValue.GetField("role").ToObject());
}
/// <summary>
/// Power Fx wraps scalar array literals such as <c>=[1, 2, 3]</c> as <c>Table({Value: 1}, ...)</c>;
/// the loop value must expose the bare scalar, not the single-column wrapper record.
/// </summary>
[Fact]
public async Task ForeachTakeNextWithSingleColumnValueRecordAsync()
{
// Arrange
const string CurrentValueName = "CurrentValue";
this.SetVariableState(CurrentValueName);
TableDataValue tableValue = DataValue.TableFromRecords(
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(1))),
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(2))),
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("Value", new NumberDataValue(3))));
Foreach model = this.CreateModel(
displayName: nameof(ForeachTakeNextWithSingleColumnValueRecordAsync),
items: ValueExpression.Literal(tableValue),
valueName: CurrentValueName,
indexName: null);
ForeachExecutor action = new(model, this.State);
// Act
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
// Assert
FormulaValue currentValue = this.State.Get(CurrentValueName);
Assert.IsNotType<RecordValue>(currentValue, exactMatch: false);
Assert.Equal(1m, currentValue.ToObject());
}
/// <summary>
/// Single-field records whose only field is NOT named <c>Value</c> are not Power Fx auto-wraps;
/// they are preserved as records so the field name remains accessible inside the loop body.
/// </summary>
[Fact]
public async Task ForeachTakeNextWithSingleFieldNonValueRecordAsync()
{
// Arrange
const string CurrentValueName = "CurrentValue";
this.SetVariableState(CurrentValueName);
TableDataValue tableValue = DataValue.TableFromRecords(
DataValue.RecordFromFields(new KeyValuePair<string, DataValue>("name", new StringDataValue("Alice"))));
Foreach model = this.CreateModel(
displayName: nameof(ForeachTakeNextWithSingleFieldNonValueRecordAsync),
items: ValueExpression.Literal(tableValue),
valueName: CurrentValueName,
indexName: null);
ForeachExecutor action = new(model, this.State);
// Act
await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync);
// Assert
RecordValue currentValue = Assert.IsType<RecordValue>(this.State.Get(CurrentValueName), exactMatch: false);
Assert.Equal("Alice", currentValue.GetField("name").ToObject());
}
[Fact]
public async Task ForeachTakeLastAsync()
{