Skip to content

IExpressiveQueryable<T>

IExpressiveQueryable<T> is the core provider-agnostic API. It enables modern C# syntax directly in LINQ chains — null-conditional operators, switch expressions, and pattern matching work in .Where(), .Select(), .OrderBy(), and more — on any IQueryable<T>.

Basic Usage

Wrap any IQueryable<T> with .AsExpressive():

db
    .Customers
    .Where(c => c.Email != null && c.Email.Length > 5)
    .Select(c => new { c.Id, Name = c.Name })
    .OrderBy(c => c.Name)
SELECT "c"."Id", "c"."Name"
FROM "Customers" AS "c"
WHERE "c"."Email" IS NOT NULL AND length("c"."Email") > 5
ORDER BY "c"."Name"
SELECT c."Id", c."Name"
FROM "Customers" AS c
WHERE c."Email" IS NOT NULL AND length(c."Email")::int > 5
ORDER BY c."Name"
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Email] IS NOT NULL AND CAST(LEN([c].[Email]) AS int) > 5
ORDER BY [c].[Name]
playground.customers.Aggregate([
    {
         "$match" : {
             "$and" : [
                {
                     "Email" : { "$ne" : null } 
                },
                {
                     "Email" : {
                         "$regularExpression" : { "pattern" : "^.{6,}$", "options" : "s" } 
                    } 
                }
            ] 
        } 
    },
    {
         "$project" : { "_id" : "$_id", "Name" : "$Name" } 
    },
    {
         "$sort" : { "Name" : 1 } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "YrX72KxcAj3MsRonpdg9+uwBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<T0> __Polyfill_OrderBy_3e61_17_5<T0, T1>(
            this global::ExpressiveSharp.IExpressiveQueryable<T0> source,
            global::System.Func<T0, T1> __func)
        {
            // Source: c => c.Name
            var i3e6117c5_p_c = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "c");
            var i3e6117c5_expr_0 = global::System.Linq.Expressions.Expression.Property(i3e6117c5_p_c, typeof(T0).GetProperty("Name", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // c.Name
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6117c5_expr_0, i3e6117c5_p_c);
            return (global::ExpressiveSharp.IExpressiveQueryable<T0>)(object)
                global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                    global::System.Linq.Queryable.OrderBy(
                        (global::System.Linq.IQueryable<T0>)(object)source,
                        __lambda));
        }
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "YrX72KxcAj3MsRonpdg9+r4BAABfX1NuaXBwZXQuY3M=")]
        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, Name = c.Name }
            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_3 = typeof(T1).GetConstructors()[0];
            var i3e6116c5_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6116c5_expr_3, new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_1, i3e6116c5_expr_2 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Id"), typeof(T1).GetProperty("Name") });
            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, "YrX72KxcAj3MsRonpdg9+ocBAABfX1NuaXBwZXQuY3M=")]
        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 => c.Email != null && c.Email.Length > 5
            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_4 = global::System.Linq.Expressions.Expression.Constant(null, typeof(object)); // null
            var i3e6115c5_expr_3 = global::System.Linq.Expressions.Expression.Convert(i3e6115c5_expr_4, typeof(string));
            var i3e6115c5_expr_1 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6115c5_expr_2, i3e6115c5_expr_3);
            var i3e6115c5_expr_7 = 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_6 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_expr_7, typeof(string).GetProperty("Length", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance));
            var i3e6115c5_expr_8 = global::System.Linq.Expressions.Expression.Constant(5, typeof(int)); // 5
            var i3e6115c5_expr_5 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, i3e6115c5_expr_6, i3e6115c5_expr_8);
            var i3e6115c5_expr_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, i3e6115c5_expr_1, i3e6115c5_expr_5);
            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) { }
    }
}

The source generator intercepts these calls at compile time and rewrites the delegate lambdas to proper expression trees. There is no runtime overhead from delegate-to-expression conversion.

How It Works

When you call .AsExpressive(), you get back an IExpressiveQueryable<T> wrapper. This wrapper exposes the same LINQ methods as IQueryable<T>, but they accept Func<...> delegates instead of Expression<Func<...>>.

At compile time, the PolyfillInterceptorGenerator uses C# 13 method interceptors to replace each call site with code that:

  1. Converts the delegate lambda into an Expression<Func<...>> using Expression.* factory calls
  2. Forwards the expression to the underlying IQueryable<T> LINQ method

The delegate stubs are never actually called at runtime — they are completely replaced by the interceptor.

Available LINQ Methods

Most common Queryable methods are supported:

Filtering:Where, Any, All

Projection:Select, SelectMany

Ordering:OrderBy, OrderByDescending, ThenBy, ThenByDescending

Grouping:GroupBy

Joins:Join, GroupJoin, Zip

Aggregation:Sum, Average, Min, Max, Count, LongCount

Element access:First, FirstOrDefault, Single, SingleOrDefault, Last, LastOrDefault (and their predicate overloads)

Set operations:ExceptBy, IntersectBy, UnionBy, DistinctBy

Chain-preserving operators (return IExpressiveQueryable<T>): Take, Skip, Distinct, Reverse, DefaultIfEmpty, Append, Prepend, Concat, Union, Intersect, Except, SkipWhile, TakeWhile

Comparer overloads (IEqualityComparer<T>, IComparer<T>) are also supported.

.NET 9 / .NET 10 Additional Methods

On .NET 9 and later: CountBy, AggregateBy, Index.

On .NET 10 and later (in addition to the above): LeftJoin, RightJoin.

Pattern Matching and Switch Expressions

Switch expressions, null-conditional operators, and pattern matching compose naturally in the chain:

db
    .Orders
    .Select(o => new
    {
        o.Id,
        Tier = o.Status switch
        {
            OrderStatus.Paid => "Confirmed",
            OrderStatus.Shipped => "Out for delivery",
            OrderStatus.Delivered => "Complete",
            _ => "Pending"
        }
    })
.param set @Paid 1
.param set @Shipped 2
.param set @Delivered 3

SELECT "o"."Id", CASE
    WHEN "o"."Status" = @Paid THEN 'Confirmed'
    WHEN "o"."Status" = @Shipped THEN 'Out for delivery'
    WHEN "o"."Status" = @Delivered THEN 'Complete'
    ELSE 'Pending'
END AS "Tier"
FROM "Orders" AS "o"
-- @Paid='1'
-- @Shipped='2'
-- @Delivered='3'
SELECT o."Id", CASE
    WHEN o."Status" = @Paid THEN 'Confirmed'
    WHEN o."Status" = @Shipped THEN 'Out for delivery'
    WHEN o."Status" = @Delivered THEN 'Complete'
    ELSE 'Pending'
END AS "Tier"
FROM "Orders" AS o
DECLARE @Paid int = 1;
DECLARE @Shipped int = 2;
DECLARE @Delivered int = 3;

SELECT [o].[Id], CASE
    WHEN [o].[Status] = @Paid THEN N'Confirmed'
    WHEN [o].[Status] = @Shipped THEN N'Out for delivery'
    WHEN [o].[Status] = @Delivered THEN N'Complete'
    ELSE N'Pending'
END AS [Tier]
FROM [Orders] AS [o]
playground.orders.Aggregate([
    {
         "$project" : {
             "_id" : "$_id",
            "Tier" : {
                 "$cond" : {
                     "if" : {
                         "$eq" : ["$Status", 1] 
                    },
                    "then" : "Confirmed",
                    "else" : {
                         "$cond" : {
                             "if" : {
                                 "$eq" : ["$Status", 2] 
                            },
                            "then" : "Out for delivery",
                            "else" : {
                                 "$cond" : {
                                     "if" : {
                                         "$eq" : ["$Status", 3] 
                                    },
                                    "then" : "Complete",
                                    "else" : "Pending" 
                                } 
                            } 
                        } 
                    } 
                } 
            } 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "L4/zH+rJJhKQrkeCRTxcG4QBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<T1> __Polyfill_Select_3e61_15_5<T0, T1>(
            this global::ExpressiveSharp.IExpressiveQueryable<T0> source,
            global::System.Func<T0, T1> __func)
        {
            // Source: o => new { o.Id, Tier = o.Status switch {     OrderStatus.Paid => "Confirmed",     OrderStatus.Shipped => "Out for delivery",     OrderStatus.Delivered => "Complete",     _ => "Pending" } }
            var i3e6115c5_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "o");
            var i3e6115c5_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_p_o, typeof(T0).GetProperty("Id", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Id
            var i3e6115c5_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_p_o, typeof(T0).GetProperty("Status", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Status
            var i3e6115c5_expr_3 = global::System.Linq.Expressions.Expression.Constant("Pending", typeof(string)); // "Pending"
            var i3e6115c5_expr_5 = 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 i3e6115c5_expr_4 = global::System.Linq.Expressions.Expression.Equal(i3e6115c5_expr_2, i3e6115c5_expr_5);
            var i3e6115c5_expr_6 = global::System.Linq.Expressions.Expression.Constant("Complete", typeof(string)); // "Complete"
            var i3e6115c5_expr_7 = global::System.Linq.Expressions.Expression.Condition(i3e6115c5_expr_4, i3e6115c5_expr_6, i3e6115c5_expr_3, typeof(string));
            var i3e6115c5_expr_9 = 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 i3e6115c5_expr_8 = global::System.Linq.Expressions.Expression.Equal(i3e6115c5_expr_2, i3e6115c5_expr_9);
            var i3e6115c5_expr_10 = global::System.Linq.Expressions.Expression.Constant("Out for delivery", typeof(string)); // "Out for delivery"
            var i3e6115c5_expr_11 = global::System.Linq.Expressions.Expression.Condition(i3e6115c5_expr_8, i3e6115c5_expr_10, i3e6115c5_expr_7, typeof(string));
            var i3e6115c5_expr_13 = 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 i3e6115c5_expr_12 = global::System.Linq.Expressions.Expression.Equal(i3e6115c5_expr_2, i3e6115c5_expr_13);
            var i3e6115c5_expr_14 = global::System.Linq.Expressions.Expression.Constant("Confirmed", typeof(string)); // "Confirmed"
            var i3e6115c5_expr_15 = global::System.Linq.Expressions.Expression.Condition(i3e6115c5_expr_12, i3e6115c5_expr_14, i3e6115c5_expr_11, typeof(string));
            var i3e6115c5_expr_16 = typeof(T1).GetConstructors()[0];
            var i3e6115c5_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6115c5_expr_16, new global::System.Linq.Expressions.Expression[] { i3e6115c5_expr_1, i3e6115c5_expr_15 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Id"), typeof(T1).GetProperty("Tier") });
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6115c5_expr_0, i3e6115c5_p_o);
            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) { }
    }
}

EF Core: Include and ThenInclude

When using IExpressiveQueryable<T> with EF Core, Include and ThenInclude are fully supported with chain continuity:

csharp
var orders = ctx.Set<Order>()
    .AsExpressive()
    .Include(o => o.Customer)
    .ThenInclude(c => c.Orders)
    .Where(o => o.Customer?.Email != null)
    .ToList();

The Include/ThenInclude calls return IIncludableExpressiveQueryable<TEntity, TProperty>, a hybrid interface that preserves both the includable chain and the rewritable chain.

INFO

Include and ThenInclude accept standard Expression<Func<...>> lambdas (not rewritten delegates), since navigation property paths do not typically need modern syntax. The chain continuity ensures you can seamlessly go from Include/ThenInclude back to rewritable LINQ methods like Where and Select.

EF Core: Async Lambda Methods

All EF Core async methods that accept a lambda predicate or selector are supported on IExpressiveQueryable<T>:

Async predicates:AnyAsync, AllAsync, CountAsync, LongCountAsync

Async element access:FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, SingleAsync, SingleOrDefaultAsync

Async aggregation:SumAsync (all numeric types), AverageAsync (all numeric types), MinAsync, MaxAsync

csharp
var hasExpensive = await ctx.Set<Order>()
    .AsExpressive()
    .AnyAsync(o => o.Price switch
    {
        >= 100 => true,
        _      => false,
    });

var total = await ctx.Set<Order>()
    .AsExpressive()
    .SumAsync(o => o.Customer?.Email != null ? o.Price : 0);

These async methods are forwarded to EntityFrameworkQueryableExtensions at compile time via the [PolyfillTarget] attribute.

EF Core: Chain Continuity Stubs

The following EF Core operations preserve the IExpressiveQueryable<T> chain, so you can continue using modern syntax after calling them:

  • AsNoTracking(), AsNoTrackingWithIdentityResolution(), AsTracking()
  • IgnoreQueryFilters(), IgnoreAutoIncludes()
  • TagWith(tag), TagWithCallSite()
csharp
var orders = ctx.Set<Order>()
    .AsExpressive()
    .AsNoTracking()
    .IgnoreQueryFilters()
    .TagWith("Admin query")
    .Where(o => o.Customer?.Email != null)
    .ToList();

EF Core: Bulk Updates with ExecuteUpdate

INFO

Requires the ExpressiveSharp.EntityFrameworkCore.RelationalExtensions package and .UseExpressives(o => o.UseRelationalExtensions()) configuration. Available on EF Core 8 and 9. On EF Core 10+, ExecuteUpdate natively accepts delegates — use ExpressionPolyfill.Create() for modern syntax in individual SetProperty value expressions.

ExecuteUpdate and ExecuteUpdateAsync are supported on IExpressiveQueryable<T>, enabling modern C# syntax inside SetProperty value expressions — which is normally impossible in expression trees:

csharp
ctx.ExpressiveSet<Order>()
    .ExecuteUpdate(s => s
        .SetProperty(o => o.Status, o => o.Price switch
        {
            > 100 => OrderStatus.Paid,
            > 50  => OrderStatus.Pending,
            _     => OrderStatus.Refunded
        }));

This generates a single SQL UPDATE with CASE WHEN and COALESCE expressions — no entity loading required.

ExecuteDelete works out of the box on IExpressiveQueryable<T> without any stubs (it has no lambda parameter).

IAsyncEnumerable Support

IExpressiveQueryable<T> supports AsAsyncEnumerable() for streaming results:

csharp
await foreach (var order in ctx.Set<Order>()
    .AsExpressive()
    .Where(o => o.Customer?.Name != null)
    .AsAsyncEnumerable())
{
    Console.WriteLine(order.Id);
}

Choosing the Right Entry Point

Entry pointWhen to use
.AsExpressive() on IQueryable<T>Any provider (EF Core, MongoDB, custom, in-memory)
ExpressiveDbSet<T> on DbContextEF Core — preferred, also triggers [Expressive] expansion via UseExpressives()
.AsExpressive() on IMongoCollection<T>MongoDB
ExpressionPolyfill.Create(...)You need a bare Expression<T> (no queryable involved)

TIP

For EF Core projects, ExpressiveDbSet<T> is the most convenient option — it combines both [Expressive] expansion and modern syntax in one API. Use .AsExpressive() when you need modern syntax on a non-EF Core IQueryable<T> or want explicit control over the wrapping.

Next Steps

Released under the MIT License.