Expression Transformers
ExpressiveSharp generates faithful expression trees that mirror the original C# code. Transformers adapt these trees for specific consumers -- for example, stripping null-check ternaries that SQL databases handle natively, or inlining block variables that EF Core cannot process.
The IExpressionTreeTransformer Interface
All transformers implement the IExpressionTreeTransformer interface:
namespace ExpressiveSharp;
public interface IExpressionTreeTransformer
{
Expression Transform(Expression expression);
}Implementations are pure functions that take an expression tree and return a transformed version. They must not have side effects.
Built-in Transformers
ExpressiveSharp ships with four built-in transformers in the ExpressiveSharp.Transformers namespace, plus one additional transformer in the RelationalExtensions package.
1. RemoveNullConditionalPatterns
Namespace: ExpressiveSharp.Transformers
Strips null-check ternaries generated by null-conditional operators. Matches the pattern x != null ? x.Member : default(T) and simplifies it to x.Member.
Before:
Customer != null ? Customer.Email : default(string)After:
Customer.EmailThis is safe for SQL databases where NULL propagation is handled natively by the database engine. See Null-Conditional Rewrite for a detailed explanation.
2. FlattenBlockExpressions
Namespace: ExpressiveSharp.Transformers
Inlines block-local variables at their use sites and removes Expression.Block nodes. Produces a single expression that typical LINQ providers (including EF Core) can translate.
Before:
Block([threshold], [
Assign(threshold, Quantity * 10),
threshold > 100 ? "Bulk" : "Regular"
])After:
(Quantity * 10) > 100 ? "Bulk" : "Regular"INFO
Variables that are assigned multiple times are not inlined -- the block is left as-is in those cases. This transformer is required for any LINQ provider that does not support BlockExpression (which includes EF Core).
3. FlattenTupleComparisons
Namespace: ExpressiveSharp.Transformers
Replaces ValueTuple field access on inline tuple construction with the underlying arguments. This enables tuple comparisons to be translated as individual column comparisons in SQL.
Before:
new ValueTuple<double, int>(Price, Quantity).Item1 == 50.0After:
Price == 50.0This allows C# code like (Price, Quantity) == (50.0, 5) to translate to Price == 50.0 AND Quantity == 5 instead of requiring ValueTuple construction, which LINQ providers cannot translate.
4. ConvertLoopsToLinq
Namespace: ExpressiveSharp.Transformers
Converts loop expressions (produced by the emitter for foreach and for loops in block-bodied members) into equivalent LINQ method calls that LINQ providers can translate to SQL.
Recognized patterns:
| Loop Pattern | LINQ Equivalent |
|---|---|
acc = acc + expr(x) | collection.Sum(x => expr) |
acc = acc + 1 | collection.Count() |
if (cond) acc = acc + 1 | collection.Count(x => cond) |
if (cond) acc = acc + expr | collection.Where(x => cond).Sum(x => expr) |
if (cond) found = true | collection.Any(x => cond) |
if (!cond) all = false | collection.All(x => cond) |
acc = Math.Min(acc, expr) | collection.Min(x => expr) |
acc = Math.Max(acc, expr) | collection.Max(x => expr) |
list.Add(expr) | collection.Select(x => expr).ToList() |
WARNING
Loops that do not match any recognized pattern will throw InvalidOperationException at runtime. If you encounter this, rewrite the loop as a LINQ expression in an expression-bodied member.
5. RewriteIndexedSelectToRowNumber
Namespace: ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.TransformersPackage: ExpressiveSharp.EntityFrameworkCore.RelationalExtensions
Converts the indexed Select overload (Queryable.Select(source, (elem, index) => body)) into a single-parameter Select where references to the index parameter are replaced with (int)(WindowFunction.RowNumber() - 1).
Before:
orders.Select((o, index) => new { o.Id, RowIndex = index })After (expression):
orders.Select(o => new { o.Id, RowIndex = (int)(ROW_NUMBER() - 1) })This allows indexed Select queries to translate to SQL using ROW_NUMBER() OVER().
TIP
This transformer is contributed by the RelationalExtensions plugin and is only available when you install ExpressiveSharp.EntityFrameworkCore.RelationalExtensions.
Applying Transformers
Per-Member via Transformers Property
Apply transformers to specific [Expressive] members using the Transformers attribute property:
[Expressive(Transformers = new[] { typeof(RemoveNullConditionalPatterns) })]
public string? CustomerName => Customer?.Name;
[Expressive(Transformers = new[] {
typeof(RemoveNullConditionalPatterns),
typeof(FlattenBlockExpressions)
})]
public string GetLabel() => /* ... */;Each transformer type must have a parameterless constructor. The transformers are applied in order when the expression is resolved at runtime.
Global via UseExpressives() (EF Core)
When you call UseExpressives() on your DbContextOptionsBuilder, all four core transformers are applied automatically to every query:
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite(connection)
.UseExpressives()
.Options;This applies (in this order):
ConvertLoopsToLinqRemoveNullConditionalPatternsFlattenTupleComparisonsFlattenBlockExpressions
No per-member configuration is needed when using UseExpressives().
Global via ExpressiveOptions.Default
Register transformers globally so all ExpandExpressives() calls apply them, even without EF Core:
using ExpressiveSharp.Services;
ExpressiveOptions.Default.AddTransformers(
new RemoveNullConditionalPatterns(),
new FlattenBlockExpressions());
// All subsequent ExpandExpressives() calls use these transformers
Expression<Func<Order, string?>> expr = o => o.CustomerName;
var expanded = expr.ExpandExpressives(); // transformers applied automaticallyYou can also clear registered transformers:
ExpressiveOptions.Default.ClearTransformers();Manual Application via ExpandExpressives()
Pass transformer instances directly when expanding:
var expanded = expr.ExpandExpressives(
new RemoveNullConditionalPatterns(),
new FlattenTupleComparisons());Plugin-Contributed Transformers
Plugins implementing IExpressivePlugin can contribute additional transformers to the EF Core pipeline:
public interface IExpressivePlugin
{
void ApplyServices(IServiceCollection services);
IExpressionTreeTransformer[] GetTransformers() => [];
}Register plugins during setup:
options.UseExpressives(o => o.AddPlugin(new MyPlugin()));The RelationalExtensions package uses this mechanism to add RewriteIndexedSelectToRowNumber to the transformer pipeline:
options.UseExpressives(o => o.UseRelationalExtensions());Writing Custom Transformers
Implement IExpressionTreeTransformer to create your own transformer. Most custom transformers extend ExpressionVisitor for easy tree traversal:
using System.Linq.Expressions;
using ExpressiveSharp;
public class MyCustomTransformer : ExpressionVisitor, IExpressionTreeTransformer
{
public Expression Transform(Expression expression)
=> Visit(expression);
protected override Expression VisitBinary(BinaryExpression node)
{
// Custom rewrite logic here
return base.VisitBinary(node);
}
}Apply it via the attribute:
[Expressive(Transformers = new[] { typeof(MyCustomTransformer) })]
public double Total => Price * Quantity;Or at runtime:
expr.ExpandExpressives(new MyCustomTransformer());Or register it globally:
ExpressiveOptions.Default.AddTransformers(new MyCustomTransformer());Or contribute it via a plugin:
public class MyPlugin : IExpressivePlugin
{
public void ApplyServices(IServiceCollection services) { }
public IExpressionTreeTransformer[] GetTransformers()
=> [new MyCustomTransformer()];
}