Skip to content

External Member Mapping

This recipe shows how to use [ExpressiveFor] and [ExpressiveForConstructor] to provide expression-tree bodies for members on types you do not own -- BCL methods, third-party libraries, or your own members that cannot use [Expressive] directly. This enables those members to be translated to SQL by EF Core.

When to Use [ExpressiveFor]

Use [ExpressiveFor] when:

  • The member is on a BCL type (Math, string, DateTime, etc.) and EF Core does not translate it
  • The member is on a third-party library type you cannot modify
  • The member is on your own type but uses logic that cannot be expressed as an [Expressive] body (reflection, I/O, etc.) and you want to provide a SQL-friendly alternative
  • You want to override how a specific member translates to SQL

INFO

If a member already has [Expressive], adding [ExpressiveFor] targeting it is a compile error (EXP0019). [ExpressiveFor] is specifically for members that do not have [Expressive].

Static Method: Math.Clamp

Math.Clamp is a BCL method that some providers cannot translate. Provide an expression-tree equivalent:

db
    .Products
    .Select(p => new { p.Id, ClampedPrice = Math.Clamp(p.ListPrice, 20m, 100m) })

// Setup
static class MathMappings
{
    [ExpressiveSharp.Mapping.ExpressiveFor(typeof(Math), nameof(Math.Clamp))]
    static decimal Clamp(decimal value, decimal min, decimal max)
        => value < min ? min : (value > max ? max : value);
}
SELECT "p"."Id", CASE
    WHEN ef_compare("p"."ListPrice", '20.0') < 0 THEN '20.0'
    WHEN ef_compare("p"."ListPrice", '100.0') > 0 THEN '100.0'
    ELSE "p"."ListPrice"
END AS "ClampedPrice"
FROM "Products" AS "p"
SELECT p."Id", CASE
    WHEN p."ListPrice" < 20.0 THEN 20.0
    WHEN p."ListPrice" > 100.0 THEN 100.0
    ELSE p."ListPrice"
END AS "ClampedPrice"
FROM "Products" AS p
SELECT [p].[Id], CASE
    WHEN [p].[ListPrice] < 20.0 THEN 20.0
    WHEN [p].[ListPrice] > 100.0 THEN 100.0
    ELSE [p].[ListPrice]
END AS [ClampedPrice]
FROM [Products] AS [p]
playground.products.Aggregate([
    {
         "$project" : {
             "_id" : "$_id",
            "ClampedPrice" : {
                 "$cond" : {
                     "if" : {
                         "$lt" : [
                            "$ListPrice",
                            { "$numberDecimal" : "20" }
                        ] 
                    },
                    "then" : { "$numberDecimal" : "20" },
                    "else" : {
                         "$cond" : {
                             "if" : {
                                 "$gt" : [
                                    "$ListPrice",
                                    { "$numberDecimal" : "100" }
                                ] 
                            },
                            "then" : { "$numberDecimal" : "100" },
                            "else" : "$ListPrice" 
                        } 
                    } 
                } 
            } 
        } 
    }
])
// === System_Math.Clamp_P0_decimal_P1_decimal_P2_decimal.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Linq;
using System.Linq.Expressions;
using ExpressiveSharp;
using ExpressiveSharp.EntityFrameworkCore;
using ExpressiveSharp.Docs.PlaygroundModel.Webshop;
using System;

namespace ExpressiveSharp.Generated
{
    static partial class System_Math 
    {
        // [ExpressiveSharp.Mapping.ExpressiveFor(typeof(Math), nameof(Math.Clamp))]
        // static decimal Clamp(decimal value, decimal min, decimal max) => value < min ? min : (value > max ? max : value);
        static global::System.Linq.Expressions.Expression<global::System.Func<decimal, decimal, decimal, decimal>> Clamp_P0_decimal_P1_decimal_P2_decimal_Expression() 
        {
            var p_value = global::System.Linq.Expressions.Expression.Parameter(typeof(decimal), "value");
            var p_min = global::System.Linq.Expressions.Expression.Parameter(typeof(decimal), "min");
            var p_max = global::System.Linq.Expressions.Expression.Parameter(typeof(decimal), "max");
            var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThan, p_value, p_min); // value < min
            var expr_3 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, p_value, p_max); // value > max
            var expr_2 = global::System.Linq.Expressions.Expression.Condition(expr_3, p_max, p_value, typeof(decimal));
            var expr_0 = global::System.Linq.Expressions.Expression.Condition(expr_1, p_min, expr_2, typeof(decimal));
            return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<decimal, decimal, decimal, decimal>>(expr_0, p_value, p_min, p_max);
        }
    }
}


// === System_Math.Attributes.g.cs ===
// <auto-generated/>

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    static partial class System_Math { }
}


// === ExpressionRegistry.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    internal static class ExpressionRegistry
    {
        private static Dictionary<nint, LambdaExpression> Build()
        {
            const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
            var map = new Dictionary<nint, LambdaExpression>();
            
            Register(map, typeof(global::System.Math).GetMethod("Clamp", allFlags, null, new global::System.Type[] { typeof(decimal), typeof(decimal), typeof(decimal) }, null), "ExpressiveSharp.Generated.System_Math", "Clamp_P0_decimal_P1_decimal_P2_decimal_Expression");
            
            return map;
        }
        
        private static volatile Dictionary<nint, LambdaExpression> _map = Build();
        
        internal static void ResetMap() => _map = Build();
        
        public static LambdaExpression TryGet(MemberInfo member)
        {
            var handle = member switch
            {
                MethodInfo m      => (nint?)m.MethodHandle.Value,
                PropertyInfo p    => p.GetMethod?.MethodHandle.Value,
                ConstructorInfo c => (nint?)c.MethodHandle.Value,
                _                 => null
            };
            
            return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
        }
        
        private static void Register(Dictionary<nint, LambdaExpression> map, MethodBase m, string exprClass, string exprMethodName)
        {
            if (m is null) return;
            var exprType = m.DeclaringType?.Assembly.GetType(exprClass) ?? typeof(ExpressionRegistry).Assembly.GetType(exprClass);
            var exprMethod = exprType?.GetMethod(exprMethodName, BindingFlags.Static | BindingFlags.NonPublic);
            if (exprMethod is null) return;
            var expr = (LambdaExpression)exprMethod.Invoke(null, null)!;
            
            // Apply declared transformers from the generated class (if any)
            const string expressionSuffix = "_Expression";
            if (exprMethodName.EndsWith(expressionSuffix, StringComparison.Ordinal))
            {
                var transformersSuffix = exprMethodName.Substring(0, exprMethodName.Length - expressionSuffix.Length) + "_Transformers";
                var transformersMethod = exprType.GetMethod(transformersSuffix, BindingFlags.Static | BindingFlags.NonPublic);
                if (transformersMethod?.Invoke(null, null) is global::ExpressiveSharp.IExpressionTreeTransformer[] transformers)
                {
                    Expression transformed = expr;
                    foreach (var t in transformers) transformed = t.Transform(transformed);
                    if (transformed is LambdaExpression lambdaResult) expr = lambdaResult;
                }
            }
            
            map[m.MethodHandle.Value] = expr;
        }
    }
}


// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "qd6yJQRqX8ToldxYu8e3EoEBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<T1> __Polyfill_Select_3e61_14_23<T0, T1>(
            this global::ExpressiveSharp.IExpressiveQueryable<T0> source,
            global::System.Func<T0, T1> __func)
        {
            // Source: p => new { p.Id, ClampedPrice = Math.Clamp(p.ListPrice, 20m, 100m) }
            var i3e6114c23_p_p = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "p");
            var i3e6114c23_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6114c23_p_p, typeof(T0).GetProperty("Id", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // p.Id
            var i3e6114c23_expr_3 = global::System.Linq.Expressions.Expression.Property(i3e6114c23_p_p, typeof(T0).GetProperty("ListPrice", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // p.ListPrice
            var i3e6114c23_expr_4 = global::System.Linq.Expressions.Expression.Constant(20m, typeof(decimal)); // 20m
            var i3e6114c23_expr_5 = global::System.Linq.Expressions.Expression.Constant(100m, typeof(decimal)); // 100m
            var i3e6114c23_expr_2 = global::System.Linq.Expressions.Expression.Call(typeof(global::System.Math).GetMethod("Clamp", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(decimal), typeof(decimal), typeof(decimal) }, null), new global::System.Linq.Expressions.Expression[] { i3e6114c23_expr_3, i3e6114c23_expr_4, i3e6114c23_expr_5 });
            var i3e6114c23_expr_6 = typeof(T1).GetConstructors()[0];
            var i3e6114c23_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6114c23_expr_6, new global::System.Linq.Expressions.Expression[] { i3e6114c23_expr_1, i3e6114c23_expr_2 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Id"), typeof(T1).GetProperty("ClampedPrice") });
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6114c23_expr_0, i3e6114c23_p_p);
            return (global::ExpressiveSharp.IExpressiveQueryable<T1>)(object)
                global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                    global::System.Linq.Queryable.Select(
                        (global::System.Linq.IQueryable<T0>)(object)source,
                        __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) { }
    }
}

TIP

The call site is unchanged -- you still write Math.Clamp(...). The ExpressiveReplacer detects the mapping at runtime and substitutes the ternary expression automatically.

Static Method: string.IsNullOrWhiteSpace

Another common BCL method that some providers cannot translate:

db
    .Customers
    .Where(c => !string.IsNullOrWhiteSpace(c.Email))

// Setup
static class StringMappings
{
    [ExpressiveSharp.Mapping.ExpressiveFor(typeof(string), nameof(string.IsNullOrWhiteSpace))]
    static bool IsNullOrWhiteSpace(string? s)
        => s == null || s.Trim().Length == 0;
}
SELECT "c"."Id", "c"."Country", "c"."Email", "c"."JoinedAt", "c"."Name"
FROM "Customers" AS "c"
WHERE "c"."Email" IS NOT NULL AND length(trim("c"."Email")) <> 0
SELECT c."Id", c."Country", c."Email", c."JoinedAt", c."Name"
FROM "Customers" AS c
WHERE c."Email" IS NOT NULL AND length(btrim(c."Email", E' \t\n\r'))::int <> 0
SELECT [c].[Id], [c].[Country], [c].[Email], [c].[JoinedAt], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Email] IS NOT NULL AND CAST(LEN(LTRIM(RTRIM([c].[Email]))) AS int) <> 0
playground.customers.Aggregate([
    {
         "$match" : {
             "$nor" : [
                {
                     "$or" : [
                        { "Email" : null },
                        {
                             "Email" : {
                                 "$regularExpression" : { "pattern" : "^\\s*(?!\\s).{0}(?<!\\s)\\s*$", "options" : "s" } 
                            } 
                        }
                    ] 
                }
            ] 
        } 
    }
])
// === System_String.IsNullOrWhiteSpace_P0_string.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Linq;
using System.Linq.Expressions;
using ExpressiveSharp;
using ExpressiveSharp.EntityFrameworkCore;
using ExpressiveSharp.Docs.PlaygroundModel.Webshop;
using System;

namespace ExpressiveSharp.Generated
{
    static partial class System_String 
    {
        // [ExpressiveSharp.Mapping.ExpressiveFor(typeof(string), nameof(string.IsNullOrWhiteSpace))]
        // static bool IsNullOrWhiteSpace(string? s) => s == null || s.Trim().Length == 0;
        static global::System.Linq.Expressions.Expression<global::System.Func<string, bool>> IsNullOrWhiteSpace_P0_string_Expression() 
        {
            var p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "s");
            var expr_3 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var expr_2 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(string));
            var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, p_s, expr_2);
            var expr_6 = global::System.Linq.Expressions.Expression.Call(p_s, typeof(string).GetMethod("Trim", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] {  }, null), global::System.Array.Empty<global::System.Linq.Expressions.Expression>()); // s.Trim()
            var expr_5 = global::System.Linq.Expressions.Expression.Property(expr_6, typeof(string).GetProperty("Length", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance));
            var expr_7 = global::System.Linq.Expressions.Expression.Constant(0, typeof(int)); // 0
            var expr_4 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_5, expr_7);
            var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.OrElse, expr_1, expr_4);
            return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<string, bool>>(expr_0, p_s);
        }
    }
}


// === System_String.Attributes.g.cs ===
// <auto-generated/>

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    static partial class System_String { }
}


// === ExpressionRegistry.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    internal static class ExpressionRegistry
    {
        private static Dictionary<nint, LambdaExpression> Build()
        {
            const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
            var map = new Dictionary<nint, LambdaExpression>();
            
            Register(map, typeof(string).GetMethod("IsNullOrWhiteSpace", allFlags, null, new global::System.Type[] { typeof(string) }, null), "ExpressiveSharp.Generated.System_String", "IsNullOrWhiteSpace_P0_string_Expression");
            
            return map;
        }
        
        private static volatile Dictionary<nint, LambdaExpression> _map = Build();
        
        internal static void ResetMap() => _map = Build();
        
        public static LambdaExpression TryGet(MemberInfo member)
        {
            var handle = member switch
            {
                MethodInfo m      => (nint?)m.MethodHandle.Value,
                PropertyInfo p    => p.GetMethod?.MethodHandle.Value,
                ConstructorInfo c => (nint?)c.MethodHandle.Value,
                _                 => null
            };
            
            return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
        }
        
        private static void Register(Dictionary<nint, LambdaExpression> map, MethodBase m, string exprClass, string exprMethodName)
        {
            if (m is null) return;
            var exprType = m.DeclaringType?.Assembly.GetType(exprClass) ?? typeof(ExpressionRegistry).Assembly.GetType(exprClass);
            var exprMethod = exprType?.GetMethod(exprMethodName, BindingFlags.Static | BindingFlags.NonPublic);
            if (exprMethod is null) return;
            var expr = (LambdaExpression)exprMethod.Invoke(null, null)!;
            
            // Apply declared transformers from the generated class (if any)
            const string expressionSuffix = "_Expression";
            if (exprMethodName.EndsWith(expressionSuffix, StringComparison.Ordinal))
            {
                var transformersSuffix = exprMethodName.Substring(0, exprMethodName.Length - expressionSuffix.Length) + "_Transformers";
                var transformersMethod = exprType.GetMethod(transformersSuffix, BindingFlags.Static | BindingFlags.NonPublic);
                if (transformersMethod?.Invoke(null, null) is global::ExpressiveSharp.IExpressionTreeTransformer[] transformers)
                {
                    Expression transformed = expr;
                    foreach (var t in transformers) transformed = t.Transform(transformed);
                    if (transformed is LambdaExpression lambdaResult) expr = lambdaResult;
                }
            }
            
            map[m.MethodHandle.Value] = expr;
        }
    }
}


// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "FBvD6Wrz9WHBYTSxvTd4YYIBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> __Polyfill_Where_3e61_14_24(
            this global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> source,
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool> __func)
        {
            // Source: c => !string.IsNullOrWhiteSpace(c.Email)
            var i3e6114c24_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer), "c");
            var i3e6114c24_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6114c24_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 i3e6114c24_expr_1 = global::System.Linq.Expressions.Expression.Call(typeof(string).GetMethod("IsNullOrWhiteSpace", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(string) }, null), new global::System.Linq.Expressions.Expression[] { i3e6114c24_expr_2 });
            var i3e6114c24_expr_0 = global::System.Linq.Expressions.Expression.MakeUnary(global::System.Linq.Expressions.ExpressionType.Not, i3e6114c24_expr_1, typeof(bool));
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool>>(i3e6114c24_expr_0, i3e6114c24_p_c);
            return global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                global::System.Linq.Queryable.Where(
                    (global::System.Linq.IQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer>)source,
                    __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) { }
    }
}

Instance Members on Your Own Type

For instance properties or methods, the first parameter of the stub is the receiver. You can use this to provide a SQL-friendly alternative for a property whose body relies on non-translatable logic:

csharp
using ExpressiveSharp.Mapping;

public class Person
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";

    // This property uses string interpolation and Trim() -- works with [Expressive],
    // but imagine it used reflection or other non-translatable logic
    public string FullName => $"{FirstName} {LastName}".Trim().ToUpper();
}

static class PersonMappings
{
    // Provide a SQL-friendly alternative
    [ExpressiveFor(typeof(Person), nameof(Person.FullName))]
    static string FullName(Person p)
        => $"{p.FirstName} {p.LastName}";
}
csharp
var names = dbContext.People
    .OrderBy(p => p.FullName)
    .Select(p => new { p.Id, p.FullName })
    .ToList();

[ExpressiveForConstructor] for Constructors

When you need to provide an expression-tree body for a constructor on a type you do not own:

csharp
using ExpressiveSharp.Mapping;

// External DTO from a shared package
public class OrderDto
{
    public int Id { get; set; }
    public string Name { get; set; } = "";

    public OrderDto(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

// Provide the expression-tree body
[ExpressiveForConstructor(typeof(OrderDto))]
static OrderDto CreateOrderDto(int id, string name)
    => new OrderDto { Id = id, Name = name };
csharp
var dtos = dbContext.Orders
    .Select(o => new OrderDto(o.Id, o.Status.ToString()))
    .ToList();

Combining with EF Core Queries

[ExpressiveFor] mappings integrate seamlessly with UseExpressives() and ExpressiveDbSet<T>. Here we combine Math.Clamp on a numeric field with string.IsNullOrWhiteSpace on a nullable string field:

db
    .Customers
    .Where(c => !string.IsNullOrWhiteSpace(c.Email))
    .Select(c => new { c.Id, c.Name, Label = c.Country ?? "Unknown" })

// Setup
static class Mappings
{
    [ExpressiveSharp.Mapping.ExpressiveFor(typeof(string), nameof(string.IsNullOrWhiteSpace))]
    static bool IsNullOrWhiteSpace(string? s)
        => s == null || s.Trim().Length == 0;
}
SELECT "c"."Id", "c"."Name", COALESCE("c"."Country", 'Unknown') AS "Label"
FROM "Customers" AS "c"
WHERE "c"."Email" IS NOT NULL AND length(trim("c"."Email")) <> 0
SELECT c."Id", c."Name", COALESCE(c."Country", 'Unknown') AS "Label"
FROM "Customers" AS c
WHERE c."Email" IS NOT NULL AND length(btrim(c."Email", E' \t\n\r'))::int <> 0
SELECT [c].[Id], [c].[Name], COALESCE([c].[Country], N'Unknown') AS [Label]
FROM [Customers] AS [c]
WHERE [c].[Email] IS NOT NULL AND CAST(LEN(LTRIM(RTRIM([c].[Email]))) AS int) <> 0
playground.customers.Aggregate([
    {
         "$match" : {
             "$nor" : [
                {
                     "$or" : [
                        { "Email" : null },
                        {
                             "Email" : {
                                 "$regularExpression" : { "pattern" : "^\\s*(?!\\s).{0}(?<!\\s)\\s*$", "options" : "s" } 
                            } 
                        }
                    ] 
                }
            ] 
        } 
    },
    {
         "$project" : {
             "_id" : "$_id",
            "Name" : "$Name",
            "Label" : {
                 "$ifNull" : ["$Country", "Unknown"] 
            } 
        } 
    }
])
// === System_String.IsNullOrWhiteSpace_P0_string.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Linq;
using System.Linq.Expressions;
using ExpressiveSharp;
using ExpressiveSharp.EntityFrameworkCore;
using ExpressiveSharp.Docs.PlaygroundModel.Webshop;
using System;

namespace ExpressiveSharp.Generated
{
    static partial class System_String 
    {
        // [ExpressiveSharp.Mapping.ExpressiveFor(typeof(string), nameof(string.IsNullOrWhiteSpace))]
        // static bool IsNullOrWhiteSpace(string? s) => s == null || s.Trim().Length == 0;
        static global::System.Linq.Expressions.Expression<global::System.Func<string, bool>> IsNullOrWhiteSpace_P0_string_Expression() 
        {
            var p_s = global::System.Linq.Expressions.Expression.Parameter(typeof(string), "s");
            var expr_3 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var expr_2 = global::System.Linq.Expressions.Expression.Convert(expr_3, typeof(string));
            var expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, p_s, expr_2);
            var expr_6 = global::System.Linq.Expressions.Expression.Call(p_s, typeof(string).GetMethod("Trim", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance, null, new global::System.Type[] {  }, null), global::System.Array.Empty<global::System.Linq.Expressions.Expression>()); // s.Trim()
            var expr_5 = global::System.Linq.Expressions.Expression.Property(expr_6, typeof(string).GetProperty("Length", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance));
            var expr_7 = global::System.Linq.Expressions.Expression.Constant(0, typeof(int)); // 0
            var expr_4 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_5, expr_7);
            var expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.OrElse, expr_1, expr_4);
            return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<string, bool>>(expr_0, p_s);
        }
    }
}


// === System_String.Attributes.g.cs ===
// <auto-generated/>

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    static partial class System_String { }
}


// === ExpressionRegistry.g.cs ===
// <auto-generated/>
#nullable disable

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressiveSharp.Generated
{
    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    internal static class ExpressionRegistry
    {
        private static Dictionary<nint, LambdaExpression> Build()
        {
            const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
            var map = new Dictionary<nint, LambdaExpression>();
            
            Register(map, typeof(string).GetMethod("IsNullOrWhiteSpace", allFlags, null, new global::System.Type[] { typeof(string) }, null), "ExpressiveSharp.Generated.System_String", "IsNullOrWhiteSpace_P0_string_Expression");
            
            return map;
        }
        
        private static volatile Dictionary<nint, LambdaExpression> _map = Build();
        
        internal static void ResetMap() => _map = Build();
        
        public static LambdaExpression TryGet(MemberInfo member)
        {
            var handle = member switch
            {
                MethodInfo m      => (nint?)m.MethodHandle.Value,
                PropertyInfo p    => p.GetMethod?.MethodHandle.Value,
                ConstructorInfo c => (nint?)c.MethodHandle.Value,
                _                 => null
            };
            
            return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;
        }
        
        private static void Register(Dictionary<nint, LambdaExpression> map, MethodBase m, string exprClass, string exprMethodName)
        {
            if (m is null) return;
            var exprType = m.DeclaringType?.Assembly.GetType(exprClass) ?? typeof(ExpressionRegistry).Assembly.GetType(exprClass);
            var exprMethod = exprType?.GetMethod(exprMethodName, BindingFlags.Static | BindingFlags.NonPublic);
            if (exprMethod is null) return;
            var expr = (LambdaExpression)exprMethod.Invoke(null, null)!;
            
            // Apply declared transformers from the generated class (if any)
            const string expressionSuffix = "_Expression";
            if (exprMethodName.EndsWith(expressionSuffix, StringComparison.Ordinal))
            {
                var transformersSuffix = exprMethodName.Substring(0, exprMethodName.Length - expressionSuffix.Length) + "_Transformers";
                var transformersMethod = exprType.GetMethod(transformersSuffix, BindingFlags.Static | BindingFlags.NonPublic);
                if (transformersMethod?.Invoke(null, null) is global::ExpressiveSharp.IExpressionTreeTransformer[] transformers)
                {
                    Expression transformed = expr;
                    foreach (var t in transformers) transformed = t.Transform(transformed);
                    if (transformed is LambdaExpression lambdaResult) expr = lambdaResult;
                }
            }
            
            map[m.MethodHandle.Value] = expr;
        }
    }
}


// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ONg/v7QlB8sQMUcVN1oqX7wBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<T1> __Polyfill_Select_3e61_16_5<T0, T1>(
            this global::ExpressiveSharp.IExpressiveQueryable<T0> source,
            global::System.Func<T0, T1> __func)
        {
            // Source: c => new { c.Id, c.Name, Label = c.Country ?? "Unknown" }
            var i3e6116c5_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "c");
            var i3e6116c5_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_c, typeof(T0).GetProperty("Id", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Id
            var i3e6116c5_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_c, typeof(T0).GetProperty("Name", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Name
            var i3e6116c5_expr_4 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_c, typeof(T0).GetProperty("Country", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Country
            var i3e6116c5_expr_5 = global::System.Linq.Expressions.Expression.Constant("Unknown", typeof(string)); // "Unknown"
            var i3e6116c5_expr_3 = global::System.Linq.Expressions.Expression.Coalesce(i3e6116c5_expr_4, i3e6116c5_expr_5);
            var i3e6116c5_expr_6 = typeof(T1).GetConstructors()[0];
            var i3e6116c5_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6116c5_expr_6, new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_1, i3e6116c5_expr_2, i3e6116c5_expr_3 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Id"), typeof(T1).GetProperty("Name"), typeof(T1).GetProperty("Label") });
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6116c5_expr_0, i3e6116c5_p_c);
            return (global::ExpressiveSharp.IExpressiveQueryable<T1>)(object)
                global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                    global::System.Linq.Queryable.Select(
                        (global::System.Linq.IQueryable<T0>)(object)source,
                        __lambda));
        }
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "ONg/v7QlB8sQMUcVN1oqX4cBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> __Polyfill_Where_3e61_15_5(
            this global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer> source,
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool> __func)
        {
            // Source: c => !string.IsNullOrWhiteSpace(c.Email)
            var i3e6115c5_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer), "c");
            var i3e6115c5_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_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 i3e6115c5_expr_1 = global::System.Linq.Expressions.Expression.Call(typeof(string).GetMethod("IsNullOrWhiteSpace", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(string) }, null), new global::System.Linq.Expressions.Expression[] { i3e6115c5_expr_2 });
            var i3e6115c5_expr_0 = global::System.Linq.Expressions.Expression.MakeUnary(global::System.Linq.Expressions.ExpressionType.Not, i3e6115c5_expr_1, typeof(bool));
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer, bool>>(i3e6115c5_expr_0, i3e6115c5_p_c);
            return global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                global::System.Linq.Queryable.Where(
                    (global::System.Linq.IQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Customer>)source,
                    __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) { }
    }
}

Common Use Cases

Math Functions

csharp
using ExpressiveSharp.Mapping;

static class MathMappings
{
    [ExpressiveFor(typeof(Math), nameof(Math.Clamp))]
    static double Clamp(double value, double min, double max)
        => value < min ? min : (value > max ? max : value);

    [ExpressiveFor(typeof(Math), nameof(Math.Abs))]
    static int Abs(int value)
        => value < 0 ? -value : value;

    [ExpressiveFor(typeof(Math), nameof(Math.Sign))]
    static int Sign(double value)
        => value > 0 ? 1 : (value < 0 ? -1 : 0);
}

String Helpers

csharp
using ExpressiveSharp.Mapping;

static class StringMappings
{
    [ExpressiveFor(typeof(string), nameof(string.IsNullOrEmpty))]
    static bool IsNullOrEmpty(string? s)
        => s == null || s.Length == 0;

    [ExpressiveFor(typeof(string), nameof(string.IsNullOrWhiteSpace))]
    static bool IsNullOrWhiteSpace(string? s)
        => s == null || s.Trim().Length == 0;
}

DateTime Calculations

csharp
using ExpressiveSharp.Mapping;

static class DateTimeMappings
{
    // Custom helper method on your utility class
    [ExpressiveFor(typeof(DateTimeHelpers), nameof(DateTimeHelpers.DaysBetween))]
    static int DaysBetween(DateTimeHelpers _, DateTime start, DateTime end)
        => (end - start).Days;
}
CodeDescription
EXP0014[ExpressiveFor] target type not found
EXP0015[ExpressiveFor] target member not found on the specified type
EXP0017Return type of stub does not match target member's return type
EXP0019Target member already has [Expressive] -- use [Expressive] directly instead
EXP0020Duplicate [ExpressiveFor] mapping for the same target member

Tips

Match the signature exactly

For static methods, the stub parameters must match the target method signature. For instance members you have three options: write a static method stub whose first parameter is the receiver (e.g. static string FullName(Person p)), write an instance method stub on the target type itself where the stub's this is the receiver, or write a property stub on the target type (parameterless; this is the receiver) -- property stubs can only target other properties.

Co-locate when possible

When the target is on the same type as the stub, you can drop typeof(...) and use the single-argument form -- it targets a member on the stub's containing type. Combined with an instance property stub, this is the ergonomic shape for the case where a property has its own backing storage (a plain settable auto-property used for DTO shape, serialization, or in-memory assignment in tests) but you want queries to compute it from other columns:

csharp
public class Person
{
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";

    // Regular auto-property — can be assigned directly (for DTOs, tests, deserialization).
    public string FullName { get; set; } = "";

    // When used in a LINQ expression tree, FullName is rewritten to this body,
    // so EF Core projects it from the underlying columns instead of trying to
    // map it to a column of its own. `this` is the receiver automatically.
    [ExpressiveFor(nameof(FullName))]
    private string FullNameExpression => $"{FirstName} {LastName}";
}

A method stub (string FullNameExpression() => ...) works the same way and is appropriate when the target is a method or you need a block body.

If the property has no backing storage and the same body works at both runtime and query time, put [Expressive] directly on the property and delete the stub.

Consider [Expressive] first

Many [ExpressiveFor] use cases exist because of syntax limitations in other libraries. Since ExpressiveSharp supports switch expressions, pattern matching, string interpolation, and more, you may be able to put [Expressive] directly on the member and skip the external mapping entirely.

Placement

[ExpressiveFor] stubs can live either in a static helper class (with a static method stub) or on the target type itself as an instance method or property. Either way, they are discovered at compile time by the source generator.

See Also

Released under the MIT License.