[Expressive] Properties
Expressive properties let you define computed values on your entities using standard C# syntax, and have those computations automatically translated into SQL when used in LINQ queries.
Defining an Expressive Property
Add [Expressive] to any expression-bodied property:
using ExpressiveSharp;
public class Order
{
public double Price { get; set; }
public int Quantity { get; set; }
public Customer? Customer { get; set; }
[Expressive]
public double Total => Price * Quantity;
[Expressive]
public string? CustomerEmail => Customer?.Email;
}The source generator emits a companion Expression<Func<Order, double>> for Total and Expression<Func<Order, string?>> for CustomerEmail at compile time. When the property is used in a LINQ query, the expression tree is substituted automatically.
Using Expressive Properties in Queries
Once defined, expressive properties can be used in any part of a LINQ query.
In Select
var totals = ctx.Orders
.Select(o => new { o.Id, o.Total })
.ToList();Generated SQL:
SELECT "o"."Id",
"o"."Price" * CAST("o"."Quantity" AS REAL) AS "Total"
FROM "Orders" AS "o"In Where
var expensive = ctx.Orders
.Where(o => o.Total > 500)
.ToList();In GroupBy
var grouped = ctx.Orders
.GroupBy(o => o.CustomerEmail)
.Select(g => new { Email = g.Key, Count = g.Count() })
.ToList();In OrderBy
var sorted = ctx.Orders
.OrderByDescending(o => o.Total)
.ToList();In multiple clauses at once
var query = ctx.Orders
.Where(o => o.Total > 100)
.OrderByDescending(o => o.Total)
.Select(o => new { o.Id, o.Total, o.CustomerEmail });Composing Expressive Properties
Expressive properties can reference other expressive properties. The entire chain is expanded transitively into the final SQL:
public class Order
{
public double Price { get; set; }
public int Quantity { get; set; }
public double TaxRate { get; set; }
[Expressive]
public double Subtotal => Price * Quantity;
[Expressive]
public double Tax => Subtotal * TaxRate; // references Subtotal
[Expressive]
public double Total => Subtotal + Tax; // references Subtotal and Tax
[Expressive]
public double TotalWithTax => Total * (1 + TaxRate); // references Total
}When you query Total, the runtime expander recursively resolves Subtotal and Tax, producing a fully flattened SQL expression:
var result = ctx.Orders
.Select(o => new { o.Id, o.Total })
.ToList();SELECT "o"."Id",
("o"."Price" * CAST("o"."Quantity" AS REAL)) +
(("o"."Price" * CAST("o"."Quantity" AS REAL)) * "o"."TaxRate") AS "Total"
FROM "Orders" AS "o"All computation happens in the database -- no data is loaded into memory.
Null-Conditional Properties
The null-conditional operator ?. works naturally in expressive properties:
public class Order
{
public Customer? Customer { get; set; }
[Expressive]
public string? CustomerEmail => Customer?.Email;
[Expressive]
public string? CustomerCity => Customer?.Address?.City;
}The source generator emits a faithful null-check ternary expression. When used with EF Core and UseExpressives(), the RemoveNullConditionalPatterns transformer strips the null checks for SQL providers that handle null propagation natively.
Block-Bodied Properties
By default, [Expressive] only supports expression-bodied properties (=>). To use block bodies with if/else, local variables, and other statements, set AllowBlockBody = true:
[Expressive(AllowBlockBody = true)]
public string Category
{
get
{
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"WARNING
Block bodies are experimental. Not all constructs are supported -- while/do-while, try/catch, async/await, assignments, and ++/-- are not translatable. Use expression-bodied properties for full compatibility.
You can also enable block bodies globally for an entire project via MSBuild instead of opting in per-member:
<PropertyGroup>
<Expressive_AllowBlockBody>true</Expressive_AllowBlockBody>
</PropertyGroup>Expanding Properties Manually
You can expand [Expressive] properties manually in expression trees outside of EF Core:
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 any LINQ provider)This is useful when you work with LINQ providers other than EF Core or need to inspect the expanded expression tree.
Important Rules
- The property must be expression-bodied (using
=>) unlessAllowBlockBody = trueis set. - The expression must be translatable by your LINQ provider -- it can only use members that the provider understands (mapped columns, navigation properties, and other
[Expressive]members). - The property body has access to
this(the entity instance) and its navigation properties. - If a property has no body, the generator reports diagnostic EXP0001.
- If a property uses a block body without opting in, the generator reports diagnostic EXP0004.
Next Steps
- [Expressive] Methods -- parameterized query fragments
- Constructor Projections -- project DTOs directly in queries
- ExpressionPolyfill.Create -- inline expression trees without attributes
