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 six 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. FlattenConcatArrayCalls ​

Namespace: ExpressiveSharp.Transformers

Rewrites string.Concat(new string[] { a, b, c, ... }) into a chain of 2/3/4-arg string.Concat calls. The emitter falls back to the array overload for interpolations with 5+ parts (e.g. $"{a} {b} {c} {d} {e}"); EF Core can't translate NewArrayInit with non-constant elements, so without this rewrite those interpolations would fail to translate to SQL.


6. ReplaceThrowWithDefault ​

Namespace: ExpressiveSharp.Transformers

Replaces throw expressions with Expression.Default(T) of the same type, so providers like EF Core (which can't translate Throw to SQL) still see a node of the surrounding Coalesce/Condition shape. Common when block-bodied members contain a throw for an "impossible" branch the database never reaches.

Opt out via o.PreserveThrowExpressions() if you need the original throw semantics — useful for non-EF query providers, or when ExpandExpressives() is invoked on a tree that won't be translated to SQL.


7. 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 core transformers are applied automatically to every query:

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

This applies (in this order):

  1. ReplaceThrowWithDefault (skip with o => o.PreserveThrowExpressions())
  2. ConvertLoopsToLinq
  3. RemoveNullConditionalPatterns
  4. FlattenTupleComparisons
  5. FlattenConcatArrayCalls
  6. 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 (from ExpressiveSharp.EntityFrameworkCore) can contribute additional transformers to the EF Core pipeline. Plugins are EF-Core-only and depend on Microsoft.Extensions.DependencyInjection.IServiceCollection:

csharp
namespace ExpressiveSharp.EntityFrameworkCore;

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.