Skip to content

ExpressionPolyfill.Create

ExpressionPolyfill.Create lets you create expression trees inline using modern C# syntax -- no [Expressive] attribute needed. The source generator intercepts the call at compile time and rewrites the delegate lambda into a proper expression tree.

Basic Usage

db
    .Customers
    .Where(ExpressionPolyfill.Create((Customer c) => c.Email?.Length > 5))
SELECT "c"."Id", "c"."Country", "c"."Email", "c"."JoinedAt", "c"."Name"
FROM "Customers" AS "c"
WHERE length("c"."Email") > 5
SELECT c."Id", c."Country", c."Email", c."JoinedAt", c."Name"
FROM "Customers" AS c
WHERE length(c."Email")::int > 5
SELECT [c].[Id], [c].[Country], [c].[Email], [c].[JoinedAt], [c].[Name]
FROM [Customers] AS [c]
WHERE CAST(LEN([c].[Email]) AS int) > 5
playground.customers.Aggregate([
    {
         "$match" : {
             "$expr" : {
                 "$gt" : [
                    { "$strLenCP" : "$Email" },
                    5
                ] 
            } 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "b6SF+VlzNKX+QqT/OQb365sBAABfX1NuaXBwZXQuY3M=")]
        internal static global::System.Linq.Expressions.Expression<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool>> __Polyfill_Create_3e61_14_49(
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool> __func)
        {
            // Source: (Customer c) => c.Email?.Length > 5
            var i3e6114c49_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer), "c");
            var i3e6114c49_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6114c49_p_c, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer).GetProperty("Email", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Email
            var i3e6114c49_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6114c49_expr_1, typeof(string).GetProperty("Length", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // .Length
            var i3e6114c49_expr_3 = global::System.Linq.Expressions.Expression.Convert(i3e6114c49_expr_2, typeof(int?));
            var i3e6114c49_expr_5 = global::System.Linq.Expressions.Expression.Constant(null, typeof(string));
            var i3e6114c49_expr_6 = global::System.Linq.Expressions.Expression.NotEqual(i3e6114c49_expr_1, i3e6114c49_expr_5);
            var i3e6114c49_expr_7 = global::System.Linq.Expressions.Expression.Default(typeof(int?));
            var i3e6114c49_expr_4 = global::System.Linq.Expressions.Expression.Condition(i3e6114c49_expr_6, i3e6114c49_expr_3, i3e6114c49_expr_7, typeof(int?));
            var i3e6114c49_expr_9 = global::System.Linq.Expressions.Expression.Constant(5, typeof(int)); // 5
            var i3e6114c49_expr_8 = global::System.Linq.Expressions.Expression.Convert(i3e6114c49_expr_9, typeof(int?));
            var i3e6114c49_expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, i3e6114c49_expr_4, i3e6114c49_expr_8);
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool>>(i3e6114c49_expr_0, i3e6114c49_p_c);
            return __lambda;
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : global::System.Attribute
    {
        public InterceptsLocationAttribute(int version, string data) { }
    }
}

You write a regular lambda with modern syntax, and the source generator converts it into an Expression<Func<...>> at compile time. The result is a fully constructed expression tree that you can compile, pass to a LINQ provider, or inspect. The query tabs above show how each provider translates the resulting predicate.

A standalone usage (outside of a queryable) looks like this:

csharp
using ExpressiveSharp;

// The lambda uses ?. -- normally illegal in expression trees
var expr = ExpressionPolyfill.Create((Customer c) => c.Email?.Length);
// expr is Expression<Func<Customer, int?>>

var compiled = expr.Compile();
var result = compiled(customer);

With Transformers

You can apply expression transformers inline:

db
    .Orders
    .Where(ExpressionPolyfill.Create(
    (Order o) => o.Customer != null && o.Customer.Country == "NL",
    new ExpressiveSharp.Transformers.RemoveNullConditionalPatterns()))
SELECT "o"."Id", "o"."CustomerId", "o"."PlacedAt", "o"."Status"
FROM "Orders" AS "o"
INNER JOIN "Customers" AS "c" ON "o"."CustomerId" = "c"."Id"
WHERE "c"."Country" = 'NL'
SELECT o."Id", o."CustomerId", o."PlacedAt", o."Status"
FROM "Orders" AS o
INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id"
WHERE c."Country" = 'NL'
SELECT [o].[Id], [o].[CustomerId], [o].[PlacedAt], [o].[Status]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [c].[Country] = N'NL'
playground.orders.Aggregate([
    {
         "$match" : {
             "Customer" : { "$ne" : null },
            "Customer.Country" : "NL" 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "QNIdVPhRUXj3MGiOg/ET6pgBAABfX1NuaXBwZXQuY3M=")]
        internal static global::System.Linq.Expressions.Expression<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>> __Polyfill_Create_3e61_14_46(
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool> __func,
            params global::ExpressiveSharp.IExpressionTreeTransformer[] transformers)
        {
            // Source: (Order o) => o.Customer != null && o.Customer.Country == "NL"
            var i3e6114c46_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order), "o");
            var i3e6114c46_expr_3 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_p_o, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order).GetProperty("Customer", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Customer
            var i3e6114c46_expr_2 = global::System.Linq.Expressions.Expression.Convert(i3e6114c46_expr_3, typeof(object));
            var i3e6114c46_expr_5 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var i3e6114c46_expr_4 = global::System.Linq.Expressions.Expression.Convert(i3e6114c46_expr_5, typeof(object));
            var i3e6114c46_expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6114c46_expr_2, i3e6114c46_expr_4);
            var i3e6114c46_expr_8 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_p_o, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order).GetProperty("Customer", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Customer
            var i3e6114c46_expr_7 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_expr_8, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer).GetProperty("Country", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance));
            var i3e6114c46_expr_9 = global::System.Linq.Expressions.Expression.Constant("NL", typeof(string)); // "NL"
            var i3e6114c46_expr_6 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, i3e6114c46_expr_7, i3e6114c46_expr_9);
            var i3e6114c46_expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, i3e6114c46_expr_1, i3e6114c46_expr_6);
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>>(i3e6114c46_expr_0, i3e6114c46_p_o);
            global::System.Linq.Expressions.Expression result = __lambda;
            foreach (var t in transformers) result = t.Transform(result);
            return (global::System.Linq.Expressions.Expression<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>>)result;
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : global::System.Attribute
    {
        public InterceptsLocationAttribute(int version, string data) { }
    }
}

The generated expression tree has RemoveNullConditionalPatterns applied, stripping the null-check ternary so the expression reads o.Customer.Country directly -- suitable for providers that handle null propagation natively.

Use Cases

Ad-hoc queries

When you need modern syntax in a one-off query without decorating entity members:

db
    .Orders
    .Where(ExpressionPolyfill.Create(
    (Order o) => o.Customer != null && o.Customer.Email != null && o.Status switch
    {
        OrderStatus.Paid => true,
        OrderStatus.Shipped => true,
        OrderStatus.Delivered => true,
        _ => false,
    }))
.param set @Paid 1
.param set @Shipped 2
.param set @Delivered 3

SELECT "o"."Id", "o"."CustomerId", "o"."PlacedAt", "o"."Status"
FROM "Orders" AS "o"
INNER JOIN "Customers" AS "c" ON "o"."CustomerId" = "c"."Id"
WHERE "c"."Email" IS NOT NULL AND CASE
    WHEN "o"."Status" = @Paid THEN 1
    WHEN "o"."Status" = @Shipped THEN 1
    WHEN "o"."Status" = @Delivered THEN 1
    ELSE 0
END
-- @Paid='1'
-- @Shipped='2'
-- @Delivered='3'
SELECT o."Id", o."CustomerId", o."PlacedAt", o."Status"
FROM "Orders" AS o
INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id"
WHERE c."Email" IS NOT NULL AND CASE
    WHEN o."Status" = @Paid THEN TRUE
    WHEN o."Status" = @Shipped THEN TRUE
    WHEN o."Status" = @Delivered THEN TRUE
    ELSE FALSE
END
DECLARE @Paid int = 1;
DECLARE @Shipped int = 2;
DECLARE @Delivered int = 3;

SELECT [o].[Id], [o].[CustomerId], [o].[PlacedAt], [o].[Status]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [c].[Email] IS NOT NULL AND CASE
    WHEN [o].[Status] = @Paid THEN CAST(1 AS bit)
    WHEN [o].[Status] = @Shipped THEN CAST(1 AS bit)
    WHEN [o].[Status] = @Delivered THEN CAST(1 AS bit)
    ELSE CAST(0 AS bit)
END = CAST(1 AS bit)
playground.orders.Aggregate([
    {
         "$match" : {
             "$and" : [
                {
                     "Customer" : { "$ne" : null } 
                },
                {
                     "Customer.Email" : { "$ne" : null } 
                },
                {
                     "$expr" : {
                         "$cond" : {
                             "if" : {
                                 "$eq" : ["$Status", 1] 
                            },
                            "then" : true,
                            "else" : {
                                 "$cond" : {
                                     "if" : {
                                         "$eq" : ["$Status", 2] 
                                    },
                                    "then" : true,
                                    "else" : {
                                         "$cond" : {
                                             "if" : {
                                                 "$eq" : ["$Status", 3] 
                                            },
                                            "then" : true,
                                            "else" : false 
                                        } 
                                    } 
                                } 
                            } 
                        } 
                    } 
                }
            ] 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "rRm3jfzhj3I+3C4WvIulHpgBAABfX1NuaXBwZXQuY3M=")]
        internal static global::System.Linq.Expressions.Expression<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>> __Polyfill_Create_3e61_14_46(
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool> __func)
        {
            // Source: (Order o) => o.Customer != null && o.Customer.Email != null && o.Status switch {     OrderStatus.Paid => true,     OrderStatus.Shipped => true,     OrderStatus.Delivered => true,     _ => false, }
            var i3e6114c46_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order), "o");
            var i3e6114c46_expr_4 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_p_o, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order).GetProperty("Customer", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Customer
            var i3e6114c46_expr_3 = global::System.Linq.Expressions.Expression.Convert(i3e6114c46_expr_4, typeof(object));
            var i3e6114c46_expr_6 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var i3e6114c46_expr_5 = global::System.Linq.Expressions.Expression.Convert(i3e6114c46_expr_6, typeof(object));
            var i3e6114c46_expr_2 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6114c46_expr_3, i3e6114c46_expr_5);
            var i3e6114c46_expr_9 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_p_o, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order).GetProperty("Customer", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Customer
            var i3e6114c46_expr_8 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_expr_9, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer).GetProperty("Email", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance));
            var i3e6114c46_expr_11 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var i3e6114c46_expr_10 = global::System.Linq.Expressions.Expression.Convert(i3e6114c46_expr_11, typeof(string));
            var i3e6114c46_expr_7 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6114c46_expr_8, i3e6114c46_expr_10);
            var i3e6114c46_expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, i3e6114c46_expr_2, i3e6114c46_expr_7);
            var i3e6114c46_expr_12 = global::System.Linq.Expressions.Expression.Property(i3e6114c46_p_o, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order).GetProperty("Status", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Status
            var i3e6114c46_expr_13 = global::System.Linq.Expressions.Expression.Constant(false, typeof(bool)); // false
            var i3e6114c46_expr_15 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.OrderStatus).GetField("Delivered", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // OrderStatus.Delivered
            var i3e6114c46_expr_14 = global::System.Linq.Expressions.Expression.Equal(i3e6114c46_expr_12, i3e6114c46_expr_15);
            var i3e6114c46_expr_16 = global::System.Linq.Expressions.Expression.Constant(true, typeof(bool)); // true
            var i3e6114c46_expr_17 = global::System.Linq.Expressions.Expression.Condition(i3e6114c46_expr_14, i3e6114c46_expr_16, i3e6114c46_expr_13, typeof(bool));
            var i3e6114c46_expr_19 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.OrderStatus).GetField("Shipped", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // OrderStatus.Shipped
            var i3e6114c46_expr_18 = global::System.Linq.Expressions.Expression.Equal(i3e6114c46_expr_12, i3e6114c46_expr_19);
            var i3e6114c46_expr_20 = global::System.Linq.Expressions.Expression.Constant(true, typeof(bool)); // true
            var i3e6114c46_expr_21 = global::System.Linq.Expressions.Expression.Condition(i3e6114c46_expr_18, i3e6114c46_expr_20, i3e6114c46_expr_17, typeof(bool));
            var i3e6114c46_expr_23 = global::System.Linq.Expressions.Expression.Field(null, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.OrderStatus).GetField("Paid", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)); // OrderStatus.Paid
            var i3e6114c46_expr_22 = global::System.Linq.Expressions.Expression.Equal(i3e6114c46_expr_12, i3e6114c46_expr_23);
            var i3e6114c46_expr_24 = global::System.Linq.Expressions.Expression.Constant(true, typeof(bool)); // true
            var i3e6114c46_expr_25 = global::System.Linq.Expressions.Expression.Condition(i3e6114c46_expr_22, i3e6114c46_expr_24, i3e6114c46_expr_21, typeof(bool));
            var i3e6114c46_expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, i3e6114c46_expr_1, i3e6114c46_expr_25);
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>>(i3e6114c46_expr_0, i3e6114c46_p_o);
            return __lambda;
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : global::System.Attribute
    {
        public InterceptsLocationAttribute(int version, string data) { }
    }
}

Testing

Build expression trees for unit tests without worrying about expression tree restrictions:

csharp
var expr = ExpressionPolyfill.Create(
    (string? s) => s?.Trim().ToUpper() ?? "EMPTY");

var compiled = expr.Compile();
Assert.AreEqual("HELLO", compiled("  hello  "));
Assert.AreEqual("EMPTY", compiled(null));

Standalone expression trees

Use modern syntax in expression trees that are not tied to any LINQ provider:

csharp
var selector = ExpressionPolyfill.Create(
    (int x) => x switch
    {
        > 0 => "positive",
        0   => "zero",
        _   => "negative",
    });

// Inspect the expression tree
Console.WriteLine(selector.Body);

How It Works

ExpressionPolyfill.Create is defined as a regular method that accepts a Func<...> delegate. At compile time, the PolyfillInterceptorGenerator uses C# 13 method interceptors to replace the call site with code that constructs the equivalent Expression<Func<...>> using Expression.* factory calls.

The method itself is never actually called at runtime -- the interceptor completely replaces it. This means:

  • No runtime overhead from delegate-to-expression conversion
  • All expression trees are built at compile time
  • The full range of modern C# syntax is available

Comparison with [Expressive]

[Expressive]ExpressionPolyfill.Create
ScopeEntity members (properties, methods, constructors)Any inline lambda
ReusabilityHigh -- define once, use in any queryOne-off -- scoped to the call site
CompositionCan reference other [Expressive] membersStandalone
RegistrationAutomatic via expression registryNot registered -- returns the expression directly
Best forComputed properties and reusable query fragmentsAd-hoc queries, testing, standalone expressions

TIP

Use [Expressive] when the logic belongs on an entity and will be reused across multiple queries. Use ExpressionPolyfill.Create for one-off expressions or when you need modern syntax outside of an entity context.

Supported Syntax

ExpressionPolyfill.Create supports the same modern C# features as [Expressive]:

  • Null-conditional ?. (member access and indexer)
  • Switch expressions with pattern matching
  • String interpolation
  • Tuple literals
  • List patterns and index/range
  • with expressions (records)
  • Collection expressions
  • Checked arithmetic

Next Steps

Released under the MIT License.