From 481653bfebbececc4cb1e44319036d4f7409ff11 Mon Sep 17 00:00:00 2001 From: venti <1308199824@qq.com> Date: Sat, 30 May 2026 14:59:36 +0800 Subject: [PATCH] fix: preserve all fields in foreach loop value for multi-field records When a Foreach action iterates over a table with multi-field records, the loop value variable only received the first field of each record due to using .Properties.Values.First() instead of converting the whole record via ToFormula(). Changed to use value.ToFormula() which preserves all fields through the RecordDataValue -> ToRecordValue() conversion path. Fixes #6183 --- .../ObjectModel/ForeachExecutor.cs | 2 +- .../ObjectModel/ForeachExecutorTest.cs | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs index e6ab5e49a9..e88cd25ef6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs @@ -49,7 +49,7 @@ public ForeachExecutor(Foreach model, WorkflowFormulaState state) EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Items); if (expressionResult.Value is TableDataValue tableValue) { - this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())]; + this._values = [.. tableValue.Values.Select(value => value.ToFormula())]; } else { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs index 63d6e15bb8..0215aed659 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs @@ -291,6 +291,38 @@ public async Task ForeachRestoreWithNoSavedStateAsync() Assert.False(executor.HasValue); } + /// + /// Foreach over a table with multi-field records must preserve all fields in the loop value variable. + /// Regression test for GH-6183. + /// + [Fact] + public async Task ForeachPreservesMultiFieldRecordsAsync() + { + // Arrange + this.SetVariableState("CurrentValue"); + TableDataValue tableValue = DataValue.TableFromRecords( + DataValue.RecordFromFields( + new KeyValuePair("name", new StringDataValue("Alice")), + new KeyValuePair("role", new StringDataValue("Engineer"))), + DataValue.RecordFromFields( + new KeyValuePair("name", new StringDataValue("Bob")), + new KeyValuePair("role", new StringDataValue("Designer")))); + + Foreach model = this.CreateModel( + displayName: nameof(ForeachPreservesMultiFieldRecordsAsync), + items: ValueExpression.Literal(tableValue), + valueName: "CurrentValue", + indexName: null); + + ForeachExecutor action = new(model, this.State); + + // Act — execute the initialisation then take the first iteration. + await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync); + + // Assert — the value must be present (all fields preserved, not collapsed to first field). + Assert.True(action.HasValue); + } + /// /// Checkpoint/restore around a foreach over an empty source must roundtrip cleanly /// (zero-length PortableValue[] snapshot).