Skip to content

EF Core ​

The ExpressiveSharp.EntityFrameworkCore package provides first-class integration with Entity Framework Core for all relational providers (SQL Server, PostgreSQL, SQLite, MySQL, Oracle) and Cosmos DB. This page covers EF Core-specific setup and features.

Installation ​

bash
dotnet add package ExpressiveSharp.EntityFrameworkCore

Depends on ExpressiveSharp (core runtime) and includes Roslyn analyzers and code fixes.

UseExpressives() Configuration ​

Call UseExpressives() on your DbContextOptionsBuilder:

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

Or with dependency injection:

csharp
services.AddDbContext<MyDbContext>(options =>
    options.UseSqlServer(connectionString)
           .UseExpressives());

What UseExpressives() Does ​

UseExpressives() automatically:

  1. Expands [Expressive] member references — walks query expression trees and replaces opaque property/method accesses with the generated expression trees
  2. Marks [Expressive] properties as unmapped — adds a model convention that tells EF Core to ignore these properties in the database model (no corresponding column)
  3. Applies database-friendly transformers (in this order):
    • ReplaceThrowWithDefault — replaces throw expressions with default(T) so Coalesce/Condition shapes survive (skip with o => o.PreserveThrowExpressions())
    • ConvertLoopsToLinq — converts loop expressions to LINQ method calls
    • RemoveNullConditionalPatterns — strips null-check ternaries for SQL providers
    • FlattenTupleComparisons — rewrites tuple field access to direct comparisons
    • FlattenConcatArrayCalls — flattens string.Concat(string[]) into chained 2/3/4-arg Concat calls
    • FlattenBlockExpressions — inlines block-local variables and removes Expression.Block nodes

String interpolation format specifiers

String interpolation with format specifiers like $"{Price:F2}" generates ToString(format) at the expression tree level. EF Core cannot translate ToString(string) to SQL — in a final Select projection this silently falls back to client evaluation, but in Where, OrderBy, or other server-evaluated positions it throws at runtime. Simple interpolation without format specifiers (e.g., $"Order #{Id}") is usually server-translatable because EF Core natively translates the 2/3/4-argument string.Concat overloads to SQL concatenation. For interpolations with 5+ parts, the emitter produces string.Concat(string[]); the FlattenConcatArrayCalls transformer listed above rewrites this into EF Core-translatable Concat calls.

ExpressiveDbSet<T> ​

ExpressiveDbSet<T> is the primary API for using modern syntax directly on a DbSet<T>. It combines [Expressive] member expansion with IExpressiveQueryable<T> modern syntax support:

csharp
public class MyDbContext : DbContext
{
    public DbSet<Customer> CustomersRaw { get; set; }

    // Shorthand for Set<Order>().AsExpressiveDbSet()
    public ExpressiveDbSet<Order> Orders => this.ExpressiveSet<Order>();
}

TIP

this.ExpressiveSet<T>() is a convenience extension method that calls Set<T>().AsExpressiveDbSet(). You can also call .AsExpressiveDbSet() on any DbSet<T> or IQueryable<T> directly.

With ExpressiveDbSet<T>, modern C# syntax works directly — no .AsExpressive() needed:

db
    .Orders
    .Where(o => o.Customer.Email != null)
    .Select(o => new
    {
        o.Id,
        Total = o.Items.Sum(i => i.UnitPrice * i.Quantity),
        Email = o.Customer.Email
    })
SELECT "o"."Id", (
    SELECT COALESCE(ef_sum(ef_multiply("l"."UnitPrice", CAST("l"."Quantity" AS TEXT))), '0.0')
    FROM "LineItems" AS "l"
    WHERE "o"."Id" = "l"."OrderId") AS "Total", "c"."Email"
FROM "Orders" AS "o"
INNER JOIN "Customers" AS "c" ON "o"."CustomerId" = "c"."Id"
WHERE "c"."Email" IS NOT NULL
SELECT o."Id", (
    SELECT COALESCE(sum(l."UnitPrice" * l."Quantity"::numeric(18,2)), 0.0)
    FROM "LineItems" AS l
    WHERE o."Id" = l."OrderId") AS "Total", c."Email"
FROM "Orders" AS o
INNER JOIN "Customers" AS c ON o."CustomerId" = c."Id"
WHERE c."Email" IS NOT NULL
SELECT [o].[Id], (
    SELECT COALESCE(SUM([l].[UnitPrice] * CAST([l].[Quantity] AS decimal(18,2))), 0.0)
    FROM [LineItems] AS [l]
    WHERE [o].[Id] = [l].[OrderId]) AS [Total], [c].[Email]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [c].[Email] IS NOT NULL
playground.orders.Aggregate([
    {
         "$match" : {
             "Customer.Email" : { "$ne" : null } 
        } 
    },
    {
         "$project" : {
             "_id" : "$_id",
            "Total" : {
                 "$sum" : {
                     "$map" : {
                         "input" : "$Items",
                        "as" : "i",
                        "in" : {
                             "$multiply" : ["$$i.UnitPrice", "$$i.Quantity"] 
                        } 
                    } 
                } 
            },
            "Email" : "$Customer.Email" 
        } 
    }
])
// === PolyfillInterceptors_b1293e61.g.cs ===
// <auto-generated/>
#nullable disable

namespace ExpressiveSharp.Generated.Interceptors
{
    internal static partial class PolyfillInterceptors
    {
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "btdnfaB/QckkkuvgQT6sUq4BAABfX1NuaXBwZXQuY3M=")]
        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: o => new { o.Id, Total = o.Items.Sum(i => i.UnitPrice * i.Quantity), Email = o.Customer.Email }
            var i3e6116c5_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(T0), "o");
            var i3e6116c5_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_o, typeof(T0).GetProperty("Id", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Id
            var i3e6116c5_expr_3 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_o, typeof(T0).GetProperty("Items", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Items
            var i3e6116c5_p_i_4 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.LineItem), "i"); // i => i.UnitPrice * i.Quantity
            var i3e6116c5_expr_6 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_i_4, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.LineItem).GetProperty("UnitPrice", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // i.UnitPrice
            var i3e6116c5_expr_8 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_i_4, typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.LineItem).GetProperty("Quantity", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // i.Quantity
            var i3e6116c5_expr_7 = global::System.Linq.Expressions.Expression.Convert(i3e6116c5_expr_8, typeof(decimal));
            var i3e6116c5_expr_5 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Multiply, i3e6116c5_expr_6, i3e6116c5_expr_7);
            var i3e6116c5_expr_9 = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.LineItem, decimal>>(i3e6116c5_expr_5, i3e6116c5_p_i_4);
            var i3e6116c5_expr_2 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Enumerable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Sum" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.GetGenericArguments()[1] == typeof(decimal))).MakeGenericMethod(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.LineItem)), new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_3, i3e6116c5_expr_9 });
            var i3e6116c5_expr_11 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_p_o, typeof(T0).GetProperty("Customer", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance)); // o.Customer
            var i3e6116c5_expr_10 = global::System.Linq.Expressions.Expression.Property(i3e6116c5_expr_11, 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 i3e6116c5_expr_12 = typeof(T1).GetConstructors()[0];
            var i3e6116c5_expr_0 = global::System.Linq.Expressions.Expression.New(i3e6116c5_expr_12, new global::System.Linq.Expressions.Expression[] { i3e6116c5_expr_1, i3e6116c5_expr_2, i3e6116c5_expr_10 }, new global::System.Reflection.MemberInfo[] { typeof(T1).GetProperty("Id"), typeof(T1).GetProperty("Total"), typeof(T1).GetProperty("Email") });
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<T0, T1>>(i3e6116c5_expr_0, i3e6116c5_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));
        }
        [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "btdnfaB/QckkkuvgQT6sUoQBAABfX1NuaXBwZXQuY3M=")]
        internal static global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order> __Polyfill_Where_3e61_15_5(
            this global::ExpressiveSharp.IExpressiveQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order> source,
            global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool> __func)
        {
            // Source: o => o.Customer.Email != null
            var i3e6115c5_p_o = global::System.Linq.Expressions.Expression.Parameter(typeof(global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order), "o");
            var i3e6115c5_expr_2 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_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 i3e6115c5_expr_1 = global::System.Linq.Expressions.Expression.Property(i3e6115c5_expr_2, 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 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_0 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.NotEqual, i3e6115c5_expr_1, i3e6115c5_expr_3);
            var __lambda = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order, bool>>(i3e6115c5_expr_0, i3e6115c5_p_o);
            return global::ExpressiveSharp.ExpressiveQueryableExtensions.AsExpressive(
                global::System.Linq.Queryable.Where(
                    (global::System.Linq.IQueryable<global::ExpressiveSharp.Docs.PlaygroundModel.Webshop.Order>)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) { }
    }
}

Include and ThenInclude ​

ExpressiveDbSet<T> preserves chain continuity across Include/ThenInclude calls:

csharp
var orders = ctx.Orders
    .Include(o => o.Customer)
    .ThenInclude(c => c.Orders)
    .Where(o => o.Customer?.Name != null)
    .ToList();

String-based includes are also supported:

csharp
var orders = ctx.Orders
    .Include("Customer")
    .Where(o => o.Customer?.Name != null)
    .ToList();

Async Methods ​

All EF Core async methods that accept lambdas are supported with modern syntax:

csharp
// Async predicate
var hasAliceOrders = await ctx.Orders
    .AnyAsync(o => o.Customer?.Name == "Alice");

// Async element access
var firstOrder = await ctx.Orders
    .FirstOrDefaultAsync(o => o.Price * o.Quantity > 100);

// Async aggregation
var totalRevenue = await ctx.Orders
    .SumAsync(o => o.Price * o.Quantity);

Supported async methods:

CategoryMethods
PredicatesAnyAsync, AllAsync, CountAsync, LongCountAsync
Element accessFirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, SingleAsync, SingleOrDefaultAsync
AggregationSumAsync (all numeric types), AverageAsync (all numeric types), MinAsync, MaxAsync

Chain Continuity Stubs ​

The following EF Core operations preserve the ExpressiveDbSet<T>/IExpressiveQueryable<T> chain:

csharp
var orders = ctx.Orders
    .AsNoTracking()
    .IgnoreQueryFilters()
    .IgnoreAutoIncludes()
    .TagWith("Dashboard query")
    .Where(o => o.Customer?.Email != null)
    .ToList();

Supported chain-preserving operations:

  • AsNoTracking(), AsNoTrackingWithIdentityResolution(), AsTracking()
  • IgnoreQueryFilters(), IgnoreAutoIncludes()
  • TagWith(tag), TagWithCallSite()

Bulk Updates with ExecuteUpdate ​

With the ExpressiveSharp.EntityFrameworkCore.RelationalExtensions package, you can use modern C# syntax inside ExecuteUpdate / ExecuteUpdateAsync:

csharp
// Requires: .UseExpressives(o => o.UseRelationalExtensions())
ctx.Orders
    .ExecuteUpdate(s => s
        .SetProperty(o => o.Status, o => o.Price switch
        {
            >= 100 => OrderStatus.Paid,
            >= 50  => OrderStatus.Pending,
            _      => OrderStatus.Refunded
        }));

Switch expressions and null-conditional operators inside SetProperty value lambdas are normally rejected by the C# compiler in expression tree contexts. The source generator converts them to CASE WHEN and COALESCE SQL expressions.

INFO

ExecuteDelete works on IExpressiveQueryable<T> / ExpressiveDbSet<T> without any additional setup — it has no lambda parameter, so no interception is needed.

WARNING

This feature is available on EF Core 8 and 9. EF Core 10 changed the ExecuteUpdate API to use Action<UpdateSettersBuilder<T>>, which natively supports modern C# syntax in the outer lambda. For inner SetProperty value expressions on EF Core 10, use ExpressionPolyfill.Create().

Plugin Architecture ​

UseExpressives() accepts an optional configuration callback for registering plugins:

csharp
options.UseExpressives(o =>
{
    o.UseRelationalExtensions();  // built-in plugin for window functions
    o.AddPlugin(new MyCustomPlugin());
});

Implementing a Custom Plugin ​

Implement IExpressivePlugin to add custom EF Core services and transformers:

csharp
public class MyPlugin : IExpressivePlugin
{
    public void ApplyServices(IServiceCollection services)
    {
        // Register custom EF Core services
    }

    public IExpressionTreeTransformer[] GetTransformers()
        => [new MyCustomTransformer()];
}

The built-in RelationalExtensions package (for window functions) uses this plugin architecture.

NuGet Packages ​

PackageDescription
ExpressiveSharpCore runtime — [Expressive] attribute, source generator, expression expansion, transformers
ExpressiveSharp.EntityFrameworkCoreEF Core integration — UseExpressives(), ExpressiveDbSet<T>, Include/ThenInclude, async methods, analyzers and code fixes
ExpressiveSharp.EntityFrameworkCore.RelationalExtensionsRelational extensions — ExecuteUpdate/ExecuteUpdateAsync with modern syntax, SQL window functions (ranking, aggregate, navigation)

INFO

The ExpressiveSharp.EntityFrameworkCore package bundles Roslyn analyzers and code fixes from ExpressiveSharp.EntityFrameworkCore.CodeFixers. These provide compile-time diagnostics and IDE quick-fix actions for common issues like missing [Expressive] attributes.

Hot Reload ​

Body edits to [Expressive] members flow through to EF Core automatically — ExpressiveQueryCompiler re-expands every query at execution time, and the new tree shape produces a fresh entry in EF Core's compiled-query cache. No restart, no manual cache clear.

WARNING

EF.CompileQuery(...) snapshots expansion at compile time. Hot-reloading an [Expressive] member does not retroactively update an already-compiled query delegate — you have to recreate it.

See Hot Reload for the full picture, including caveats around captured expression trees and rude edits.

Next Steps ​

Released under the MIT License.