Skip to content

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:

csharp
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:

csharp
Customer != null ? Customer.Email : default(string)

After:

csharp
Customer.Email

This 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:

csharp
Block([threshold], [
    Assign(threshold, Quantity * 10),
    threshold > 100 ? "Bulk" : "Regular"
])

After:

csharp
(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:

csharp
new ValueTuple<double, int>(Price, Quantity).Item1 == 50.0

After:

csharp
Price == 50.0

This 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 PatternLINQ Equivalent
acc = acc + expr(x)collection.Sum(x => expr)
acc = acc + 1collection.Count()
if (cond) acc = acc + 1collection.Count(x => cond)
if (cond) acc = acc + exprcollection.Where(x => cond).Sum(x => expr)
if (cond) found = truecollection.Any(x => cond)
if (!cond) all = falsecollection.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:

csharp
orders.Select((o, index) => new { o.Id, RowIndex = index })

After (expression):

csharp
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:

csharp
[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:

csharp
var options = new DbContextOptionsBuilder<MyDbContext>()
    .UseSqlite(connection)
    .UseExpressives()
    .Options;

This applies (in this order):

  1. ConvertLoopsToLinq
  2. RemoveNullConditionalPatterns
  3. FlattenTupleComparisons
  4. FlattenBlockExpressions

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:

csharp
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 automatically

You can also clear registered transformers:

csharp
ExpressiveOptions.Default.ClearTransformers();

Manual Application via ExpandExpressives()

Pass transformer instances directly when expanding:

csharp
var expanded = expr.ExpandExpressives(
    new RemoveNullConditionalPatterns(),
    new FlattenTupleComparisons());

Plugin-Contributed Transformers

Plugins implementing IExpressivePlugin can contribute additional transformers to the EF Core pipeline:

csharp
public interface IExpressivePlugin
{
    void ApplyServices(IServiceCollection services);
    IExpressionTreeTransformer[] GetTransformers() => [];
}

Register plugins during setup:

csharp
options.UseExpressives(o => o.AddPlugin(new MyPlugin()));

The RelationalExtensions package uses this mechanism to add RewriteIndexedSelectToRowNumber to the transformer pipeline:

csharp
options.UseExpressives(o => o.UseRelationalExtensions());

Writing Custom Transformers

Implement IExpressionTreeTransformer to create your own transformer. Most custom transformers extend ExpressionVisitor for easy tree traversal:

csharp
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:

csharp
[Expressive(Transformers = new[] { typeof(MyCustomTransformer) })]
public double Total => Price * Quantity;

Or at runtime:

csharp
expr.ExpandExpressives(new MyCustomTransformer());

Or register it globally:

csharp
ExpressiveOptions.Default.AddTransformers(new MyCustomTransformer());

Or contribute it via a plugin:

csharp
public class MyPlugin : IExpressivePlugin
{
    public void ApplyServices(IServiceCollection services) { }
    public IExpressionTreeTransformer[] GetTransformers()
        => [new MyCustomTransformer()];
}

Released under the MIT License.