Source generator that enables modern C# syntax in LINQ expression trees. All expression trees are generated at compile time with minimal runtime overhead.
There are two problems when using C# with LINQ providers like EF Core:
1. Expression tree syntax restrictions. You write a perfectly reasonable query and hit:
error CS8072: An expression tree lambda may not contain a null propagating operator
Expression trees (Expression<Func<...>>) only support a restricted subset of C# — no ?., no switch expressions, no pattern matching. So you end up writing ugly ternary chains instead of the clean code you'd write anywhere else.
2. Computed properties are opaque to LINQ providers. You define public string FullName => FirstName + " " + LastName and use it in a query — but EF Core can't see inside the property getter. It either throws a runtime translation error, or worse, silently fetches the entire entity to evaluate FullName on the client (overfetching). The only workaround is to duplicate the logic as an inline expression in every query that needs it.
ExpressiveSharp fixes this. Write natural C# and the source generator builds the expression tree at compile time:
public class Order
{
public int Id { get; set; }
public double Price { get; set; }
public int Quantity { get; set; }
public Customer? Customer { get; set; }
// Computed property — reusable in any query, translated to SQL
[Expressive]
public double Total => Price * Quantity;
// Switch expression — normally illegal in expression trees
[Expressive]
public string GetGrade() => Price switch
{
>= 100 => "Premium",
>= 50 => "Standard",
_ => "Budget",
};
}
// [Expressive] members + ?. syntax — all translated to SQL
var results = db.Orders
.AsExpressiveDbSet()
.Where(o => o.Customer?.Email != null)
.Select(o => new { o.Id, o.Total, Email = o.Customer?.Email, Grade = o.GetGrade() })
.ToList();Generated SQL (SQLite):
SELECT "o"."Id",
"o"."Price" * CAST("o"."Quantity" AS REAL) AS "Total",
"c"."Email",
CASE
WHEN "o"."Price" >= 100.0 THEN 'Premium'
WHEN "o"."Price" >= 50.0 THEN 'Standard'
ELSE 'Budget'
END AS "Grade"
FROM "Orders" AS "o"
LEFT JOIN "Customers" AS "c" ON "o"."CustomerId" = "c"."Id"
WHERE "c"."Email" IS NOT NULLdotnet add package ExpressiveSharp
# Optional: EF Core integration
dotnet add package ExpressiveSharp.EntityFrameworkCoreMark computed properties and methods with [Expressive] to generate companion expression trees. Then choose how to wire them into your queries:
| Scenario | API |
|---|---|
EF Core — modern syntax + [Expressive] expansion on DbSet |
ExpressiveDbSet<T> (or UseExpressives() for global [Expressive] expansion) |
Any IQueryable — modern syntax + [Expressive] expansion |
.WithExpressionRewrite() |
Advanced — build an Expression<T> inline, no attribute needed |
ExpressionPolyfill.Create |
Advanced — expand [Expressive] members in an existing expression tree |
.ExpandExpressives() |
Decorate properties, methods, or constructors to generate companion expression trees. Supports modern C# syntax that normally can't appear in expression trees.
public class Order
{
public Customer? Customer { get; set; }
public string? Tag { get; set; }
// Null-conditional operators
[Expressive]
public string? CustomerEmail => Customer?.Email;
// Switch expressions with pattern matching
[Expressive]
public string GetGrade() => Price switch
{
>= 100 => "Premium",
>= 50 => "Standard",
_ => "Budget",
};
// Block bodies are opt-in and experimental
[Expressive(AllowBlockBody = true)]
public string GetCategory()
{
var threshold = Quantity * 10;
if (threshold > 100) return "Bulk";
return "Regular";
}
}EF Core translates block bodies to SQL CASE expressions:
SELECT CASE
WHEN ("o"."Quantity" * 10) > 100 THEN 'Bulk'
ELSE 'Regular'
END AS "Category"
FROM "Orders" AS "o"Expand [Expressive] members manually in expression trees — this replaces o.Total (a property access) with the generated expression (o.Price * o.Quantity), so LINQ providers can translate it:
Expression<Func<Order, double>> expr = o => o.Total;
// expr body is: o.Total (opaque property access)
var expanded = expr.ExpandExpressives();
// expanded body is: o.Price * o.Quantity (translatable by EF Core / other providers)Wrap any IQueryable<T> to use modern syntax directly in LINQ chains:
var results = queryable
.WithExpressionRewrite()
.Where(o => o.Customer?.Email != null)
.Select(o => new { o.Id, Name = o.Customer?.Name ?? "Unknown" })
.OrderBy(o => o.Name)
.ToList();The source generator intercepts these calls at compile time and rewrites them to use proper expression trees — no runtime overhead.
Available LINQ methods: Where, Select, SelectMany, OrderBy, OrderByDescending, ThenBy, ThenByDescending, GroupBy.
Create expression trees inline using modern syntax — no attribute needed:
var expr = ExpressionPolyfill.Create((Order o) => o.Tag?.Length);
// expr is Expression<Func<Order, int?>>
var compiled = expr.Compile();
var result = compiled(order);With transformers:
var expr = ExpressionPolyfill.Create(
(Order o) => o.Customer?.Email,
new RemoveNullConditionalPatterns());Install ExpressiveSharp.EntityFrameworkCore and call UseExpressives() on your DbContextOptionsBuilder:
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite(connection)
.UseExpressives()
.Options;This automatically:
- Expands
[Expressive]member references in queries - Marks
[Expressive]properties as unmapped in the EF model - Applies database-friendly transformers (
ConvertLoopsToLinq,RemoveNullConditionalPatterns,FlattenTupleComparisons,FlattenBlockExpressions)
For direct access on DbSet, use ExpressiveDbSet<T>:
public class MyDbContext : DbContext
{
// Shorthand for Set<Order>().AsExpressiveDbSet()
public ExpressiveDbSet<Order> Orders => this.ExpressiveSet<Order>();
}
// Modern syntax works directly — no .WithExpressionRewrite() needed
ctx.Orders.Where(o => o.Customer?.Name == "Alice");| Feature | Status | Notes |
|---|---|---|
Null-conditional ?. (member access and indexer) |
Supported | Generates faithful null-check ternary; UseExpressives() strips it for SQL |
| Switch expressions | Supported | Translated to nested CASE/ternary |
| Pattern matching (constant, type, relational, logical, property, positional) | Supported | |
| Declaration patterns with named variables | Partial | Works in switch arms only |
| String interpolation | Supported | Converted to string.Concat calls |
| Tuple literals | Supported | |
| Enum method expansion | Supported | Expands enum extension methods into per-value ternary chains |
| C# 14 extension members | Supported | |
| List patterns (fixed-length and slice) | Supported | |
Index/range (^1, 1..3) |
Supported | |
with expressions (records) |
Supported | |
Collection expressions ([1, 2, 3], [..items]) |
Supported | |
| Dictionary indexer initializers | Supported | |
this/base references |
Supported |
| Feature | Status |
|---|---|
return, if/else, switch statements |
Supported |
| Local variable declarations (inlined) | Supported |
foreach loops (converted to LINQ) |
Supported |
for loops (array/list iteration) |
Supported |
while/do-while, try/catch, async/await |
Not supported |
Assignments, ++, -- |
Not supported |
Mark constructors with [Expressive] to generate MemberInit expressions that EF Core can translate to SQL projections:
public class OrderSummaryDto
{
public int Id { get; set; }
public string Description { get; set; } = "";
public double Total { get; set; }
public OrderSummaryDto() { }
[Expressive]
public OrderSummaryDto(int id, string description, double total)
{
Id = id;
Description = description;
Total = total;
}
}
// Constructor call is translated to SQL projection
var dtos = db.Orders
.Select(o => new OrderSummaryDto(o.Id, o.Tag ?? "N/A", o.Total))
.ToList();SELECT "o"."Id",
COALESCE("o"."Tag", 'N/A') AS "Description",
"o"."Price" * CAST("o"."Quantity" AS REAL) AS "Total"
FROM "Orders" AS "o"Property assignments, local variables, if/else, and base()/this() initializer chains are all supported.
ExpressiveSharp generates faithful expression trees that mirror the original C# code. Transformers adapt these trees for specific consumers at runtime.
RemoveNullConditionalPatterns — Strips null-check ternaries (x != null ? x.Prop : default becomes x.Prop). Useful for databases that handle null propagation natively.
FlattenBlockExpressions — Inlines block-local variables and removes Expression.Block nodes. Required for LINQ providers that don't support block expressions (including EF Core).
FlattenTupleComparisons — Replaces ValueTuple field access on inline tuple construction with the underlying arguments, so (Price, Quantity) == (50.0, 5) translates to Price == 50.0 AND Quantity == 5 instead of requiring ValueTuple construction.
ConvertLoopsToLinq — Converts loop expressions (produced by the emitter for foreach/for loops) into equivalent LINQ method calls (Sum, Count, Any, All, etc.) that LINQ providers can translate to SQL.
All four transformers are applied automatically when using UseExpressives() with EF Core. To apply them per-member without EF Core, use the Transformers property:
[Expressive(Transformers = new[] { typeof(RemoveNullConditionalPatterns) })]
public string? CustomerName => Customer?.Name;Implement IExpressionTreeTransformer to create your own:
public class MyTransformer : IExpressionTreeTransformer
{
public Expression Transform(Expression expression)
{
// Rewrite expression tree as needed
return expression;
}
}Apply via the attribute or at runtime:
[Expressive(Transformers = new[] { typeof(MyTransformer) })]
public double Total => Price * Quantity;
// Or at runtime
expr.ExpandExpressives(new MyTransformer());ExpressiveSharp uses two Roslyn source generators:
-
ExpressiveGenerator— Finds[Expressive]members, analyzes them at the semantic level (IOperation), and generatesExpression<Func<...>>factory code usingExpression.*calls. Registers them in a per-assembly expression registry for runtime lookup. -
PolyfillInterceptorGenerator— Uses C# 13 method interceptors to replaceExpressionPolyfill.Createcalls andIRewritableQueryable<T>LINQ methods at their call sites, converting lambdas to expression trees at compile time.
All expression trees are generated at compile time. There is no runtime reflection or expression compilation.
No practical impact. The source generators emit Expression.* factory calls at compile time. At runtime, ExpandExpressives() (or UseExpressives() in EF Core) replaces opaque property accesses with the pre-built expressions — this adds a small cost on first execution, but LINQ providers like EF Core cache the expanded query afterward. There is no runtime reflection, no Compile(), and no expression tree parsing.
Yes. ExpandExpressives() (and UseExpressives()) recursively resolves nested [Expressive] references. You can compose computed properties freely:
[Expressive]
public double Total => Price * Quantity;
[Expressive]
public double TotalWithTax => Total * (1 + TaxRate); // references TotalNo. The core ExpressiveSharp package works with any LINQ provider or standalone expression tree use case. The ExpressiveSharp.EntityFrameworkCore package adds EF Core-specific integration (auto-expansion, model conventions, transformers).
ExpressiveSharp is its spiritual successor. See the Migration Guide for a step-by-step walkthrough including automated code fixers.
Key improvements: broader C# syntax support (switch expressions, pattern matching, string interpolation, tuples), customizable transformer pipeline, inline expression creation via ExpressionPolyfill.Create, modern syntax in LINQ chains via IRewritableQueryable<T>, and no EF Core coupling.
| .NET 8.0 | .NET 10.0 | |
|---|---|---|
| ExpressiveSharp | C# 12 | C# 14 |
| ExpressiveSharp.EntityFrameworkCore | EF Core 8.x | EF Core 10.x |
Development docs for contributors:
- Testing Strategy — snapshot tests, functional tests, and test consumers
- IOperation to Expression Mapping — reference table for the expression tree emitter
dotnet build # Build all projects
dotnet test # Run all testsMIT