CLR & Concurrency · Lesson 7

Expression trees are data and delegates execute

Inspect expression nodes and request actual interpretation rather than claiming dynamic code generation.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

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 is an object representation of code structure, while a delegate is directly callable behavior. Both can describe the same arithmetic without being the same kind of value. The tree exposes an Add node with a Multiply node on its left, and the program then requests real interpreted execution through Compile with preferInterpretation true. That request avoids claiming runtime native-code generation in the browser. The ordinary delegate supplies an independent callable comparison. The example does not send a query to a provider or assume that arbitrary C# constructs can be represented or translated as expression nodes.

Trace the portable program

The lambda multiplies its input by two and adds one. Inspecting the tree reports node:Add and left:Multiply. Interpreting it for input three produces seven, and invoking the directly declared delegate for three also produces seven. The final lines are interpreted:7 and delegate:7. Those equal values establish the bounded arithmetic result, not identical allocation, compilation, or execution cost. Tree inspection remains possible before execution, which is useful when designing translators or validators; whether a particular external provider supports these nodes is a separate contract requiring that provider's real behavior.

Execute the actual desktop lab

The desktop project exercises the same public expression API, including the preference for interpretation, instead of claiming a tree is executable merely because it prints a familiar lambda. A tree can also be built manually from parameters, constants, and operation nodes, which is useful when the structure itself is the product. A translator may accept only a documented subset of those shapes. A directly compiled C# delegate has no general public facility that reconstructs the original source tree from its behavior. Choose the representation deliberately: an inspectable computation description and an opaque callable function solve different boundary problems even when this small example returns the same integer.

Pitfalls and boundary limits

Not every C# feature can appear in an expression tree produced by lambda conversion, and not every representable node is supported by every remote provider. Compile with an interpretation preference is a local execution request, not a request to translate SQL or prove NativeAOT publication. Execution also has a lifetime: captured objects can remain reachable through the tree or resulting delegate. These issues are independent of the arithmetic answer. The sample uses a parameterized value-only expression and small integers to avoid hidden state and overflow, so its output does not accidentally depend on provider behavior or a mutable ambient object.

Practice one: isolate behavior

First change the final constant from one to three in both computations and predict nine for input three. Inspect the root and left node kinds again and explain why those structural kinds remain Add and Multiply. Hint: a literal value can change without changing the shape of the operation tree. Then replace addition with multiplication and predict both the new result and the root node kind before running. Preserve a direct expected number instead of accepting equality between two implementations that could share the same mistaken formula.

Practice two: design and verify

For another exercise, construct the same parameterized tree manually and compare its result for zero, a negative input, and three. Hint: reuse the same ParameterExpression wherever the logical parameter is intended; unrelated parameter objects are not interchangeable just because their display names match. Add a validator that accepts only the specific arithmetic node kinds you support and rejects an unsupported shape explicitly. Keep local interpretation tests separate from any later provider integration. A validator that recognizes a shape does not prove an untested database or native deployment can execute it.

Review the evidence

Review expression use by identifying whether consumers need structure, execution, translation, or serialization, and then state which node subset and lifetime rules apply. The four baseline lines establish the actual shape and bounded local result. The desktop run confirms the public API behavior in another runtime environment, without giving a performance ranking between interpretation and direct delegates. If performance becomes relevant, measure the real workload and include tree construction and compilation costs in the intended scope. Do not infer native code generation merely from the word Compile or infer universal provider support from a successful local arithmetic calculation.

External .NET 10 SDK / desktop runtime. OS-thread experiments run only here; the .NET 9 browser companion has the narrower scope stated in the guide.

Expression trees are data and delegates execute — desktop runtime lab

Set up the SDK project

  1. Save both files at their exact paths in a fresh owned directory.
  2. Use a .NET 10 SDK, then run dotnet run --project RuntimeLab.csproj. This project intentionally uses C# 13 syntax.
  3. 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.csDownload 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;

Expression<Func<int, int>> tree = value => value * 2 + 1;
Func<int, int> direct = value => value * 2 + 1;
var body = (BinaryExpression)tree.Body;
Console.WriteLine($"node:{body.NodeType}");
Console.WriteLine($"left:{body.Left.NodeType}");
Console.WriteLine($"interpreted:{tree.Compile(preferInterpretation: true)(3)}");
Console.WriteLine($"delegate:{direct(3)}");
RuntimeLab.csprojDownload 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: node:Add left:Multiply interpreted:7 delegate:7 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;

Expression<Func<int, int>> tree = value => value * 2 + 1;
Func<int, int> direct = value => value * 2 + 1;
var body = (BinaryExpression)tree.Body;
Console.WriteLine($"node:{body.NodeType}");
Console.WriteLine($"left:{body.Left.NodeType}");
Console.WriteLine($"interpreted:{tree.Compile(preferInterpretation: true)(3)}");
Console.WriteLine($"delegate:{direct(3)}");
Open C# practice

C# practice output

node:Add
left:Multiply
interpreted:7
delegate:7
Try this
A tree can be inspected before it is executed. preferInterpretation requests an execution strategy; it does not show a database query or prove generated native code exists.

Further reading

Interview questions for this lesson