They live among us, but do we notice how wonderful they are?
Have you ever stared at a line of code and wondered: "Is this data, or is this an instruction?" If the answer is "yes," congratulations—you've had a software existential crisis. In .NET, Expressions are that middle ground where code becomes data and data becomes executable logic.
Basic Level - "Don't hurt me, I'm new"
What is an expression?
In human terms: An expression is anything that returns a value.
- 2 + 2 is an expression.
- DateTime.Now is an expression.
- Your boss saying "we need to talk" is an expression that returns a value of StressLevel.Maximum.
We use them constantly without even realizing it, especially with Expression-bodied members (those => arrows that save us from typing return and curly braces).
using System;
namespace BasicExpressions;
public class Developer
{
public string Name { get; set; } = "Junior Dev";
public int CoffeeCups { get; set; } = 0;
// Expression-bodied property: Short and sweet
public bool IsFunctional => CoffeeCups > 3;
// Expression-bodied method
public void DrinkCoffee() => CoffeeCups++;
public override string ToString() => $"Dev: {Name}, Status: {(IsFunctional ? "Coding" : "Rebooting")}";
}
public class Program
{
public static void Main()
{
var dev = new Developer();
dev.DrinkCoffee();
dev.DrinkCoffee();
dev.DrinkCoffee();
dev.DrinkCoffee();
Console.WriteLine(dev.ToString());
}
}
The Joke: If you're still using curly braces { return x; } for a single line in .NET 9, the compiler won't say anything, but a kitten somewhere in the world will stop purring.
Expression Trees: Look, but don't touch (yet)
This is where things get interesting. An Expression<Func> is not code that executes immediately. It is a data tree that describes the code.
Think of it like a cooking recipe:
Delegate (Func): This is the cooked meal. You just eat it.
Expression (Expression): This is the recipe written on paper. You can read it, swap "salt" for "sugar" to troll someone, or decide never to cook it at all.
This is exactly what Entity Framework uses to translate your C# into SQL.
using System;
using System.Linq.Expressions;
namespace IntermediateExpressions;
public class Program
{
public static void Main()
{
// This is NOT a function; it is a DESCRIPTION of a function
Expression<Func<int, bool>> isLargeNumberExp = n => n > 100;
// Let's "gut" the expression to see what's inside
var parameters = isLargeNumberExp.Parameters[0]; // 'n'
var body = (BinaryExpression)isLargeNumberExp.Body; // 'n > 100'
Console.WriteLine($"Parameter name: {parameters.Name}");
Console.WriteLine($"Left side: {body.Left}"); // n
Console.WriteLine($"Node type: {body.NodeType}"); // GreaterThan
Console.WriteLine($"Right side: {body.Right}"); // 100
// If we actually want to use it, we have to compile it "on the fly"
var compiledFunc = isLargeNumberExp.Compile();
Console.WriteLine($"Result of 150 > 100: {compiledFunc(150)}");
}
}
Tip: Use this when you need dynamic logic that depends on database fields or filters that change at runtime. But don't overdo it—reading these trees later feels like trying to decipher Egyptian hieroglyphs.
Advanced Level - "The Architect"
In .NET 9/10, optimization is king. Sometimes, you need to generate code at runtime because you don't know what you're processing until the app is already running. Here, we build expressions by hand, block by block, like LEGOs—but with the risk of blowing up the memory.
using System;
using System.Linq.Expressions;
namespace AdvancedExpressions;
public class Program
{
public static void Main()
{
// Goal: Create (salary, performance) => salary + (salary * performance) manually
var salaryParam = Expression.Parameter(typeof(double), "salary");
var perfParam = Expression.Parameter(typeof(double), "performance");
// Step 1: salary * performance
var multiplier = Expression.Multiply(salaryParam, perfParam);
// Step 2: salary + (result of multiplier)
var totalExpression = Expression.Add(salaryParam, multiplier);
// Step 3: Wrap it in a Lambda
var lambda = Expression.Lambda<Func<double, double, double>>(
totalExpression,
salaryParam,
perfParam
);
// Step 4: Compile to IL (Intermediate Language)
var calculateBonus = lambda.Compile();
// Step 5: Execute
double mySalary = 50000;
double myPerformance = 0.15; // 15% bonus
double finalPay = calculateBonus(mySalary, myPerformance);
Console.WriteLine($"The formula generated was: {lambda}");
Console.WriteLine($"Final calculated pay: {finalPay}");
}
}
How are we doing? Good? Boring? Let's recap:
- Basic: Use => to keep your code sexy and readable.
- Intermediate: Understand that Expressions = Data. This explains why LINQ sometimes fails at runtime instead of compile time.
- Advanced: Build your own trees if you want to create the next great .NET Framework, or if you simply hate the teammates who will have to maintain your code.
Expert Level - "The Logic Interceptor"
Expression Visitors: Modifying code while it's in transit
Imagine you have a filter coming from a UI, but you want to "intercept" it and change part of the logic (for example, adding a global IsDeleted == false check or masking sensitive data) without the developer knowing.
This is done with the ExpressionVisitor. It’s essentially a "Search and Replace" but for compiled logic.
using System;
using System.Linq.Expressions;
namespace ExpertExpressions;
// We inherit from ExpressionVisitor to "walk" the tree
public class MathTrollVisitor : ExpressionVisitor
{
protected override Expression VisitBinary(BinaryExpression node)
{
// If we find a Plus (+), we swap it for a Minus (-)
if (node.NodeType == ExpressionType.Add)
{
return Expression.Subtract(node.Left, node.Right);
}
return base.VisitBinary(node);
}
}
public class Program
{
public static void Main()
{
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;
Console.WriteLine($"Original: {addExpression}"); // (a, b) => (a + b)
var troll = new MathTrollVisitor();
var modifiedExp = (LambdaExpression)troll.Visit(addExpression);
Console.WriteLine($"Modified: {modifiedExp}"); // (a, b) => (a - b)
var result = ((Func<int, int, int>)modifiedExp.Compile())(10, 5);
Console.WriteLine($"Result of 10 + 5 (trolled): {result}"); // Output: 5
}
}
Note: This is how Multi-tenant systems automatically inject TenantId filters into every Entity Framework query without the dev writing .Where(x => x.TenantId == ...) every single time.
Dynamic LINQ and Caching (The Memory Tax Fighter)
Since you hate the "Memory Tax," here is the danger: Expression.Compile() is expensive. If you compile the same expression inside a loop, your CPU will cry.
In advanced scenarios, we use Expression Hashing. We analyze the tree, generate a unique hash, and store the compiled delegate in a ConcurrentDictionary.
using System;
using System.Collections.Concurrent;
using System.Linq.Expressions;
namespace GodModeExpressions;
public static class FastPropertyAccessor
{
private static readonly ConcurrentDictionary<string, Delegate> _cache = new();
public static TValue GetValue<TTarget, TValue>(TTarget target, string propName)
{
var key = $"{typeof(TTarget).FullName}_{propName}";
var func = (Func<TTarget, TValue>)_cache.GetOrAdd(key, _ => {
var param = Expression.Parameter(typeof(TTarget), "t");
var property = Expression.Property(param, propName);
return Expression.Lambda<Func<TTarget, TValue>>(property, param).Compile();
});
return func(target);
}
}
public class ServerNode
{
public string NodeName { get; set; } = "Cartago-01";
}
public class Program
{
public static void Main()
{
var server = new ServerNode();
// First call: Compiles the expression (Slow)
// Subsequent calls: Uses the cache (Fast as native code)
var name = FastPropertyAccessor.GetValue<ServerNode, string>(server, "NodeName");
Console.WriteLine($"Reflected Value: {name}");
}
}
Why .NET 10? With the push toward Native AOT (Ahead-of-Time), standard Expression.Compile() is a problem because it generates JIT code at runtime. Future versions are focusing on Source Generators that turn these Expressions into actual C# code during compilation, giving you the flexibility of Expressions with the zero-overhead of native code.
Insanity Level - "The Vertical Slice Fusion"
The Concept: Expression Splicing with Records
In a standard architecture, you have a slice for "Data" and a slice for "Business Logic." Usually, you fetch data, map it to a DTO, then process it. That's two allocations. In "Insanity Mode," we take the Data Expression and the Logic Expression and "splice" them together using Expression.Invoke or a custom ParameterReplacer. The result? One single SQL query (if using EF) or one single CPU pass that outputs a final Record.
using System;
using System.Linq;
using System.Linq.Expressions;
namespace InsanityExpressions;
// The ultra-lean result using C# Records
public record HeroDashboard(string Name, string Rank, bool IsLegendary, double PowerLevel);
public class Program
{
public static void Main()
{
// Slice 1: The Data Mapper (From raw DB entity to basic info)
Expression<Func<string, double, HeroDashboard>> dataMapper =
(name, power) => new HeroDashboard(name, "Unranked", false, power);
// Slice 2: The Business Logic (Determines if they are legendary)
// We want to "inject" this logic into the first expression
Expression<Func<double, bool>> checkLegendary = p => p > 9000;
// THE FUSION: We build a single expression that does both
var fusedExpression = FuseHeroLogic(dataMapper, checkLegendary);
Console.WriteLine($"Fused Expression: {fusedExpression}");
// Compiling and testing
var finalFunc = fusedExpression.Compile();
var result = finalFunc("Goku", 9001);
Console.WriteLine($"Name: {result.Name}");
Console.WriteLine($"Is Legendary: {result.IsLegendary}"); // True!
}
public static Expression<Func<string, double, HeroDashboard>> FuseHeroLogic(
Expression<Func<string, double, HeroDashboard>> mapper,
Expression<Func<double, bool>> logic)
{
var nameParam = mapper.Parameters[0];
var powerParam = mapper.Parameters[1];
// We "invoke" the logic expression using the power parameter of the mapper
var logicInvoke = Expression.Invoke(logic, powerParam);
// We rebuild the Record constructor, but replacing the 'false' with our logic
var ctor = typeof(HeroDashboard).GetConstructors()[0];
// This is where the magic happens: creating the Record on the fly
var newRecord = Expression.New(ctor,
nameParam,
Expression.Constant("S-Tier"), // Hardcoding a rank change
logicInvoke, // Injected Logic!
powerParam
);
return Expression.Lambda<Func<string, double, HeroDashboard>>(newRecord, nameParam, powerParam);
}
}
Note: The "Vertical Slice" Impact, Imagine your GetHeroById handler. Instead of calling a service that calls a repository, you compose the expressions from those layers into one.
- Database: "Oh, I see you need Name, Power, and a calculation of Power > 9000. I'll do that in SQL."
- Result: You get a HeroDashboard record back. No extra loops, no extra Select statements.
Summary
By the time you reach this level, you aren't just writing code; you are writing code that writes code.
- Pro: Your app is incredibly fast and memory-efficient. You bypass the "standard developer overhead."
- Con: If a Junior developer opens this file, they might think you're performing a ritual to summon a demon.
My advice? Document this like it's the recipe for eternal life. Because if you lose the context of how these expressions are fused, you'll be debugging "Parameter count mismatch" errors until 2027.
Furthermore, but no less important, respect: If you're thinking, "This is impractical" or "I'd do it differently," congratulations: you're already analyzing. But before you dismiss anything, remember that here we build bridges, not walls. If you're looking for a rigid formula, there are a thousand manuals out there; if you're looking to collaborate and expand the boundaries of what's possible, stay. We respect the code, but we respect the process even more. Keep learning wherever you want, but here... here we come to create.
By One Garlos P. 2026