Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
The runtime contract
An expression tree that refers to a captured variable is not automatically a snapshot of that variable's current value. The generated representation can contain access to the captured storage, and later execution reads its then-current contents. This program contrasts a captured zero-argument expression with an explicitly parameterized expression. Both are genuinely interpreted by the expression API. The comparison extends ordinary closure reasoning into inspectable expression structure, rather than inventing a custom evaluator. A remote query provider may apply its own translation rules, so local interpretation should not be mislabeled as proof of every provider's evaluation timing.
Trace the portable program
The captured factor begins at three, so the first interpreted call produces six. After factor changes to five, the same interpreted expression produces ten. The parameterized expression receives three explicitly and still produces six, independent of factor. Inspecting the left side of the captured multiplication reveals MemberAccess, not a literal integer constant. Output is before:6, after:10, input:6, and capture:MemberAccess. Follow the input source for each call: a common wrong answer assumes building the tree froze factor at construction, or assumes every expression must instead share the unrelated ambient variable.
Execute the actual desktop lab
The desktop project repeats actual interpreted execution of both tree forms. The captured expression's member access reaches storage associated with the captured local, whereas the parameterized expression receives its input for each invocation. A literal ConstantExpression built from an integer value would represent a different dependency choice. For mutable reference values, even a constant node can hold a reference whose object later changes, so a constant expression is not a recursive clone. Track both the expression structure and the ownership of values reachable from it when using trees as reusable computation descriptions or caching keys.
Pitfalls and boundary limits
A provider may evaluate captured values during translation, parameter extraction, or another documented stage rather than reproducing the local interpreter's exact timing. Do not use this local output to promise every query provider takes a snapshot at tree construction or reads the current value at network execution. The tree can also retain captured state for as long as it remains reachable. Caching a captured expression therefore has both correctness and lifetime implications. A parameterized form can make reuse clearer, but it still needs a deliberate contract for input types, supported operations, and any objects supplied by the caller.
Practice one: isolate behavior
First change factor to seven after the first call and predict fourteen from the retained captured computation while the explicit input three still produces six. Hint: update the storage that the tree actually references, not an unrelated local with a similar name. Then invoke the parameterized computation with five and predict ten without changing factor. Keep these two modifications separate so you can attribute each result to its real input source. Inspect the captured MemberAccess again and explain why that node is different from a literal integer constant.
Practice two: design and verify
For another exercise, manually build a tree with a ConstantExpression containing the current integer factor, then change the original local and compare that tree with the captured one. Hint: the manually supplied scalar value is a snapshot of that integer, while the closure member is a later read. Repeat the reasoning for a mutable object and decide what an independent snapshot would require. If an external provider is part of the application, add real provider tests for parameter extraction and repeated execution rather than assuming the local interpretation experiment answers its translation contract.
Review the evidence
Review reusable expression trees by listing every parameter, constant, captured member, and reachable mutable object. Determine when each input is read and who owns its lifetime. The four output lines make one captured-versus-explicit distinction observable, while the desktop project verifies genuine expression execution. Neither form is automatically a stable serialization format or a universally safe cache key. Keep tree identity and input identity distinct, and avoid describing a cached expression as immutable application state simply because its node graph itself cannot be edited through ordinary mutation APIs. If inputs include mutable collections, define whether a consumer observes their current contents or a copied snapshot, and test that exact boundary independently.
Captured expression values versus parameters — desktop runtime lab
Set up the SDK project
- Save both files at their exact paths in a fresh owned directory.
- Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
- Workers and waits are bounded; a timeout or a violated invariant fails the process. No benchmark or NativeAOT publication is implied by an ordinary run.
Download complete SDK project ↗
Project files
Program.cs
Download file ↗using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
int factor = 3;
Expression<Func<int>> captured = () => factor * 2;
var read = captured.Compile(preferInterpretation: true);
Expression<Func<int, int>> parameterized = value => value * 2;
var apply = parameterized.Compile(preferInterpretation: true);
Console.WriteLine($"before:{read()}");
factor = 5;
Console.WriteLine($"after:{read()}");
Console.WriteLine($"input:{apply(3)}");
Console.WriteLine($"capture:{((BinaryExpression)captured.Body).Left.NodeType}");
RuntimeLab.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>13.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>false</PublishAot>
</PropertyGroup>
</Project>
Expected result
Exact behavioral stdout on the stated desktop lab: before:6 after:10 input:6 capture:MemberAccess Variable diagnostics, where emitted, go to stderr and must not be treated as portable timing or allocation guarantees.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
int factor = 3;
Expression<Func<int>> captured = () => factor * 2;
var read = captured.Compile(preferInterpretation: true);
Expression<Func<int, int>> parameterized = value => value * 2;
var apply = parameterized.Compile(preferInterpretation: true);
Console.WriteLine($"before:{read()}");
factor = 5;
Console.WriteLine($"after:{read()}");
Console.WriteLine($"input:{apply(3)}");
Console.WriteLine($"capture:{((BinaryExpression)captured.Body).Left.NodeType}");
C# practice output
before:6 after:10 input:6 capture:MemberAccess
Predict execution after changing factor. An expression tree is not automatically a frozen snapshot or a universally translatable provider query.