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 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(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(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, 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.