diff --git a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs
index c265099..a9678f2 100644
--- a/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs
+++ b/Forge.TreeWalker.UnitTests/test/ForgeSchemaHelper.cs
@@ -570,5 +570,399 @@ public static class ForgeSchemaHelper
}
}
";
+
+ #region CacheVars Schemas
+
+ ///
+ /// Basic CacheVars test — static Roslyn expression used in ShouldSelect.
+ ///
+ public const string CacheVars_StaticExpression = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""myVal"": ""C#|42""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|(int)Cache.myVal == 42"",
+ ""Child"": ""CorrectValue""
+ },
+ {
+ ""Child"": ""WrongValue""
+ }
+ ]
+ },
+ ""CorrectValue"": {
+ ""Type"": ""Leaf""
+ },
+ ""WrongValue"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars referencing Session.GetOutput to extract ActionResponse data.
+ ///
+ public const string CacheVars_SessionGetOutput = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Action"",
+ ""Actions"": {
+ ""Root_CollectDiagnosticsAction"": {
+ ""Action"": ""CollectDiagnosticsAction"",
+ ""Input"": {
+ ""Command"": ""TheCommand""
+ }
+ }
+ },
+ ""CacheVars"": {
+ ""actionStatus"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"").Status""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.actionStatus == \""Success\"""",
+ ""Child"": ""Found""
+ },
+ {
+ ""Child"": ""NotFound""
+ }
+ ]
+ },
+ ""Found"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotFound"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars using UserContext.
+ ///
+ public const string CacheVars_UserContext = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""userName"": ""C#|UserContext.Name""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.userName == \""MyName\"""",
+ ""Child"": ""Found""
+ },
+ {
+ ""Child"": ""NotFound""
+ }
+ ]
+ },
+ ""Found"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotFound"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars are node-scoped — second node should NOT see first node's cache variables.
+ ///
+ public const string CacheVars_NodeScoped = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""firstNodeVar"": ""C#|99""
+ },
+ ""ChildSelector"": [
+ {
+ ""Child"": ""SecondNode""
+ }
+ ]
+ },
+ ""SecondNode"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""secondNodeCheck"": ""C#|Cache == null ? \""isolated\"" : \""leaked\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.secondNodeCheck == \""isolated\"""",
+ ""Child"": ""Isolated""
+ },
+ {
+ ""Child"": ""Leaked""
+ }
+ ]
+ },
+ ""Isolated"": {
+ ""Type"": ""Leaf""
+ },
+ ""Leaked"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars with invalid expression — should throw EvaluateDynamicPropertyException.
+ ///
+ public const string CacheVars_InvalidExpression = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""badVar"": ""C#|NonExistentObject.Property""
+ },
+ ""ChildSelector"": [
+ {
+ ""Child"": ""End""
+ }
+ ]
+ },
+ ""End"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// Multiple CacheVars on the same node.
+ ///
+ public const string CacheVars_Multiple = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""greeting"": ""C#|\""hello\"""",
+ ""target"": ""C#|\""world\"""",
+ ""suffix"": ""C#|\""!\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.greeting.ToString() == \""hello\"" && Cache.target.ToString() == \""world\"" && Cache.suffix.ToString() == \""!\"""",
+ ""Child"": ""Found""
+ },
+ {
+ ""Child"": ""NotFound""
+ }
+ ]
+ },
+ ""Found"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotFound"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars with string literal (non-Roslyn value).
+ ///
+ public const string CacheVars_StringLiteral = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""literal"": ""hello world""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.literal.ToString() == \""hello world\"""",
+ ""Child"": ""Found""
+ },
+ {
+ ""Child"": ""NotFound""
+ }
+ ]
+ },
+ ""Found"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotFound"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars with ActionResponse object value — access nested property via Cache.
+ ///
+ public const string CacheVars_ObjectValue = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Action"",
+ ""Actions"": {
+ ""Root_CollectDiagnosticsAction"": {
+ ""Action"": ""CollectDiagnosticsAction"",
+ ""Input"": {
+ ""Command"": ""TheCommand""
+ }
+ }
+ },
+ ""CacheVars"": {
+ ""response"": ""C#|Session.GetOutput(\""Root_CollectDiagnosticsAction\"")""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.response != null"",
+ ""Child"": ""Found""
+ },
+ {
+ ""Child"": ""NotFound""
+ }
+ ]
+ },
+ ""Found"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotFound"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars with boolean expression — use Cache.IsReady in ShouldSelect.
+ ///
+ public const string CacheVars_BooleanExpression = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""IsReady"": ""C#|true""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|(bool)Cache.IsReady"",
+ ""Child"": ""Ready""
+ },
+ {
+ ""Child"": ""NotReady""
+ }
+ ]
+ },
+ ""Ready"": {
+ ""Type"": ""Leaf""
+ },
+ ""NotReady"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars on multiple node types — Selection and Action.
+ ///
+ public const string CacheVars_AllNodeTypes = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""nodeType"": ""C#|\""selection\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""selection\"""",
+ ""Child"": ""ActionNode""
+ },
+ {
+ ""Child"": ""End""
+ }
+ ]
+ },
+ ""ActionNode"": {
+ ""Type"": ""Action"",
+ ""Actions"": {
+ ""ActionNode_CollectDiagnosticsAction"": {
+ ""Action"": ""CollectDiagnosticsAction"",
+ ""Input"": {
+ ""Command"": ""TheCommand""
+ }
+ }
+ },
+ ""CacheVars"": {
+ ""nodeType"": ""C#|\""action\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.nodeType.ToString() == \""action\"""",
+ ""Child"": ""End""
+ },
+ {
+ ""Child"": ""Fail""
+ }
+ ]
+ },
+ ""End"": {
+ ""Type"": ""Leaf""
+ },
+ ""Fail"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ ///
+ /// CacheVars same-named property across two nodes — confirms isolation (second node re-defines same var).
+ ///
+ public const string CacheVars_SameNameAcrossNodes = @"
+ {
+ ""Tree"": {
+ ""Root"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""status"": ""C#|\""first\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.status.ToString() == \""first\"""",
+ ""Child"": ""SecondNode""
+ },
+ {
+ ""Child"": ""Fail""
+ }
+ ]
+ },
+ ""SecondNode"": {
+ ""Type"": ""Selection"",
+ ""CacheVars"": {
+ ""status"": ""C#|\""second\""""
+ },
+ ""ChildSelector"": [
+ {
+ ""ShouldSelect"": ""C#|Cache.status.ToString() == \""second\"""",
+ ""Child"": ""End""
+ },
+ {
+ ""Child"": ""Fail""
+ }
+ ]
+ },
+ ""End"": {
+ ""Type"": ""Leaf""
+ },
+ ""Fail"": {
+ ""Type"": ""Leaf""
+ }
+ }
+ }";
+
+ #endregion CacheVars Schemas
}
-}
\ No newline at end of file
+}
diff --git a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs
index 8dbc702..4369ae8 100644
--- a/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs
+++ b/Forge.TreeWalker.UnitTests/test/TreeWalkerUnitTests.cs
@@ -1346,5 +1346,176 @@ private TreeWalkerSession InitializeSubroutineTree(SubroutineInput subroutineInp
return new TreeWalkerSession(subroutineParameters);
}
+
+ #region CacheVars
+
+ [TestMethod]
+ public void TestCacheVars_StaticExpression()
+ {
+ // Test - CacheVars with a static Roslyn expression binds value available in ShouldSelect.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StaticExpression);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("CorrectValue", currentNode, "Expected Cache.myVal == 42 to route to CorrectValue.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_SessionGetOutput()
+ {
+ // Test - CacheVars can reference Session.GetOutput to extract ActionResponse data.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_SessionGetOutput);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Found", currentNode, "Expected Cache.actionStatus == 'Success'.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_UserContext()
+ {
+ // Test - CacheVars can bind UserContext properties.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_UserContext);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Found", currentNode, "Expected Cache.userName == 'MyName'.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_NodeScoped_ClearedBetweenNodes()
+ {
+ // Test - CacheVars are scoped to the current node only. Second node should not see first node's vars.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_NodeScoped);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Isolated", currentNode, "Expected Cache to be cleared between nodes.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_InvalidExpression_ThrowsEvaluateDynamicPropertyException()
+ {
+ // Test - Invalid CacheVariable expression throws EvaluateDynamicPropertyException, status is Failed_EvaluateDynamicProperty.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_InvalidExpression);
+
+ Assert.ThrowsException(() =>
+ {
+ this.session.WalkTree("Root").GetAwaiter().GetResult();
+ });
+ Assert.AreEqual("Failed_EvaluateDynamicProperty", this.session.Status);
+ }
+
+ [TestMethod]
+ public void TestCacheVars_MultipleCacheVars()
+ {
+ // Test - Multiple CacheVars on the same node all resolve correctly.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_Multiple);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Found", currentNode, "Expected all three string cache variables to resolve correctly.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_StringLiteral()
+ {
+ // Test - CacheVars with a non-Roslyn string literal value.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StringLiteral);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Found", currentNode, "Expected string literal to be accessible as Cache.literal.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_BackwardCompat_NoCacheVars()
+ {
+ // Test - Schemas without CacheVars work identically (backward compat).
+ this.TestFromFileInitialize(filePath: TardigradeSchemaPath);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+ }
+
+ [TestMethod]
+ public void TestCacheVars_SetCachePublicApi()
+ {
+ // Test - Public SetCache API pre-fills the Cache for ShouldSelect evaluation.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_StaticExpression);
+
+ // Manually set Cache via public API before WalkTree
+ this.session.SetCache(new { myVal = 42 }).GetAwaiter().GetResult();
+
+ // The schema will evaluate its own CacheVars (overwriting), but this tests that SetCache doesn't throw.
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+ }
+
+ [TestMethod]
+ public void TestCacheVars_ObjectValue()
+ {
+ // Test - CacheVars can store an object (ActionResponse) and access it in ShouldSelect.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_ObjectValue);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Found", currentNode, "Expected Cache.response to not be null.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_BooleanExpression()
+ {
+ // Test - CacheVars with boolean value used directly in ShouldSelect.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_BooleanExpression);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("Ready", currentNode, "Expected Cache.IsReady == true to route to Ready.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_AllNodeTypes()
+ {
+ // Test - CacheVars works on Selection and Action node types.
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_AllNodeTypes);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("End", currentNode, "Expected CacheVars to work on all node types.");
+ }
+
+ [TestMethod]
+ public void TestCacheVars_SameNameAcrossNodes()
+ {
+ // Test - Same-named CacheVar across two nodes confirms isolation (each node gets fresh value).
+ this.TestInitialize(jsonSchema: ForgeSchemaHelper.CacheVars_SameNameAcrossNodes);
+
+ string actualStatus = this.session.WalkTree("Root").GetAwaiter().GetResult();
+ Assert.AreEqual("RanToCompletion", actualStatus);
+
+ string currentNode = this.session.GetCurrentTreeNode().GetAwaiter().GetResult();
+ Assert.AreEqual("End", currentNode, "Expected second node to have its own 'status' = 'second'.");
+ }
+
+ #endregion CacheVars
}
-}
\ No newline at end of file
+}
diff --git a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json
index 3af7616..feda591 100644
--- a/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json
+++ b/Forge.TreeWalker/contracts/ForgeSchemaValidationRules.json
@@ -50,6 +50,12 @@
},
"Properties": {
"type": "object"
+ },
+ "CacheVars": {
+ "type": "object",
+ "patternProperties": {
+ ".*": { "type": "string" }
+ }
}
},
"additionalProperties": false,
@@ -95,6 +101,12 @@
},
"Properties": {
"type": "object"
+ },
+ "CacheVars": {
+ "type": "object",
+ "patternProperties": {
+ ".*": { "type": "string" }
+ }
}
},
"additionalProperties": false,
@@ -156,6 +168,12 @@
},
"Properties": {
"type": "object"
+ },
+ "CacheVars": {
+ "type": "object",
+ "patternProperties": {
+ ".*": { "type": "string" }
+ }
}
},
"additionalProperties": false,
@@ -281,4 +299,4 @@
]
}
}
-}
\ No newline at end of file
+}
diff --git a/Forge.TreeWalker/contracts/ForgeTree.cs b/Forge.TreeWalker/contracts/ForgeTree.cs
index 38d9834..b69f266 100644
--- a/Forge.TreeWalker/contracts/ForgeTree.cs
+++ b/Forge.TreeWalker/contracts/ForgeTree.cs
@@ -58,6 +58,16 @@ public class TreeNode
[DataMember]
public ChildSelector[] ChildSelector { get; private set; }
+ ///
+ /// Optional cache variables that are evaluated via EvaluateDynamicProperty after actions complete
+ /// and made available in ShouldSelect expressions via the Cache object (e.g., "Cache.myVar").
+ /// Each property key is the variable name, and the value is an expression
+ /// (e.g., "C#|Session.GetOutput(\"ActionKey\").Output.PropertyName").
+ /// Cache variables are scoped to the current node only — they are cleared after SelectChild.
+ ///
+ [DataMember]
+ public dynamic CacheVars { get; private set; }
+
#region Properties used only by TreeNodeType.Action nodes
///
diff --git a/Forge.TreeWalker/src/ExpressionExecutor.cs b/Forge.TreeWalker/src/ExpressionExecutor.cs
index 51bf3bd..886f009 100644
--- a/Forge.TreeWalker/src/ExpressionExecutor.cs
+++ b/Forge.TreeWalker/src/ExpressionExecutor.cs
@@ -79,7 +79,8 @@ public ExpressionExecutor(ITreeSession session, object userContext, List d
{
UserContext = userContext,
Session = session,
- TreeInput = treeInput
+ TreeInput = treeInput,
+ Cache = null
};
this.scriptCache = scriptCache ?? new ConcurrentDictionary>();
@@ -208,6 +209,32 @@ public bool ScriptCacheContainsKey(string expression)
return this.scriptCache.ContainsKey(expression);
}
+ ///
+ /// Gets the Cache object (the evaluated CacheVars result, or null).
+ ///
+ /// The cache object.
+ public object GetCache()
+ {
+ return this.parameters.Cache;
+ }
+
+ ///
+ /// Sets the Cache object to the evaluated CacheVars result.
+ ///
+ /// The evaluated cache object from EvaluateDynamicProperty.
+ public void SetCache(object cache)
+ {
+ this.parameters.Cache = cache;
+ }
+
+ ///
+ /// Clears the Cache, resetting it to null for the next node visit.
+ ///
+ public void ClearCache()
+ {
+ this.parameters.Cache = null;
+ }
+
///
/// This class defines the global parameter that will be passed into the Roslyn expression evaluator.
///
@@ -232,6 +259,13 @@ public class CodeGenInputParams
/// For Subroutines, this is evaluated from the SubroutineInput on the schema.
///
public dynamic TreeInput { get; set; }
+
+ ///
+ /// The dynamic Cache object that holds node-scoped CacheVars.
+ /// Variables are set after actions complete and are available in ShouldSelect expressions.
+ /// Cache is cleared after SelectChild.
+ ///
+ public dynamic Cache { get; set; }
}
///
diff --git a/Forge.TreeWalker/src/TreeWalkerSession.cs b/Forge.TreeWalker/src/TreeWalkerSession.cs
index 38efbf3..92c0683 100644
--- a/Forge.TreeWalker/src/TreeWalkerSession.cs
+++ b/Forge.TreeWalker/src/TreeWalkerSession.cs
@@ -282,6 +282,17 @@ public string GetCurrentNodeSkipActionContext()
return this.currentNodeSkipActionContext;
}
+ ///
+ /// Evaluates the given CacheVars schema object and sets the result on the Cache.
+ /// This is useful for external UTs that need to pre-fill the Cache before evaluating ShouldSelect expressions.
+ ///
+ /// The CacheVars schema object to evaluate (typically from TreeNode.CacheVars).
+ public async Task SetCache(dynamic cacheVars)
+ {
+ object cacheResult = await this.EvaluateDynamicProperty(cacheVars, null).ConfigureAwait(false);
+ this.expressionExecutor.SetCache(cacheResult);
+ }
+
///
/// Signals the WalkTree and VisitNode cancellation token sources to cancel.
///
@@ -514,8 +525,17 @@ public async Task VisitNode(string treeNodeKey)
return null;
}
- // Return next child to visit, if possible.
- return await this.SelectChild(treeNode).ConfigureAwait(false);
+ // Evaluate CacheVars after actions complete, before selecting child.
+ // Uses the same EvaluateDynamicProperty pattern as Properties/Input.
+ await this.SetCache(treeNode.CacheVars).ConfigureAwait(false);
+
+ // Select next child to visit.
+ string result = await this.SelectChild(treeNode).ConfigureAwait(false);
+
+ // Clear Cache after SelectChild — locks Cache access to ShouldSelect only.
+ this.expressionExecutor.ClearCache();
+
+ return result;
}
///