[Expressive] Attribute
The ExpressiveAttribute is the primary entry point for ExpressiveSharp. Place it on any property, method, extension method, or constructor to tell the source generator to produce a companion expression tree at compile time.
Namespace
using ExpressiveSharp;Targets
| Target | Supported |
|---|---|
| Properties | Yes |
| Methods | Yes |
| Extension methods | Yes |
| Constructors | Yes |
| Indexers | No |
The attribute can be inherited by derived types (Inherited = true).
Properties
AllowBlockBody
Type: boolDefault: false
Enables block-bodied member support. Without this flag, using a block body ({ }) with [Expressive] produces error EXP0004. Setting this to true allows block bodies that support local variables, if/else, switch statements, and foreach loops.
When not explicitly set on the attribute, the MSBuild property Expressive_AllowBlockBody is used as the global default (also defaults to false).
[Expressive(AllowBlockBody = true)]
public string GetCategory()
{
var threshold = Quantity * 10;
if (threshold > 100) return "Bulk";
return "Regular";
}Or enable globally for the entire project:
<PropertyGroup>
<Expressive_AllowBlockBody>true</Expressive_AllowBlockBody>
</PropertyGroup>Transformers
Type: Type[]?Default: null
Specifies additional IExpressionTreeTransformer types to apply at runtime when the expression is resolved. Each type must have a parameterless constructor.
[Expressive(Transformers = new[] { typeof(RemoveNullConditionalPatterns) })]
public string? CustomerName => Customer?.Name;See Expression Transformers for the full list of built-in transformers and how to create custom ones.
How It Works
When the source generator encounters an [Expressive] member, it:
- Analyzes the member body at the IOperation (semantic) level
- Generates
Expression<Func<...>>factory code usingExpression.*calls - Registers the generated expression in a per-assembly expression registry
At runtime, ExpandExpressives() (or UseExpressives() in EF Core) looks up the registered expression and replaces opaque member accesses with the generated expression tree, so LINQ providers can translate them.
No NullConditionalRewriteSupport enum
Unlike Projectables, which required a per-member NullConditionalRewriteSupport enum to configure ?. handling, ExpressiveSharp always generates a faithful ternary (x != null ? x.Prop : default). If you need to strip the null checks for SQL providers, the RemoveNullConditionalPatterns transformer handles it globally. UseExpressives() applies this transformer automatically. See Null-Conditional Rewrite for details.
No ExpandEnumMethods property
ExpressiveSharp always expands enum extension methods into per-value ternary chains automatically. There is no opt-in flag needed.
No CompatibilityMode
ExpressiveSharp does not have a compatibility mode setting. Expression expansion always uses the full approach, which handles all scenarios correctly.
Using ExpandExpressives()
After marking members with [Expressive], you can manually expand them in expression trees using the .ExpandExpressives() extension method:
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)This replaces [Expressive] member references with their generated expression trees. Expansion is recursive -- if TotalWithTax references Total, both are expanded:
[Expressive]
public double Total => Price * Quantity;
[Expressive]
public double TotalWithTax => Total * (1 + TaxRate);
Expression<Func<Order, double>> expr = o => o.TotalWithTax;
var expanded = expr.ExpandExpressives();
// expanded body is: (o.Price * o.Quantity) * (1 + o.TaxRate)You can also pass transformers to ExpandExpressives():
expr.ExpandExpressives(new RemoveNullConditionalPatterns());Or register transformers globally so all calls use them:
ExpressiveOptions.Default.AddTransformers(new RemoveNullConditionalPatterns());
expr.ExpandExpressives(); // RemoveNullConditionalPatterns applied automaticallyComplete Example
public class Order
{
public int Id { get; set; }
public double Price { get; set; }
public int Quantity { get; set; }
public string? Tag { get; set; }
public Customer? Customer { get; set; }
// Simple computed property
[Expressive]
public double Total => Price * Quantity;
// Composing expressives
[Expressive]
public double TotalWithTax => Total * (1 + 0.08);
// Null-conditional operators -- always generates faithful ternary
[Expressive]
public string? CustomerEmail => Customer?.Email;
// Switch expressions with pattern matching
[Expressive]
public string GetGrade() => Price switch
{
>= 100 => "Premium",
>= 50 => "Standard",
_ => "Budget",
};
// Per-member transformer
[Expressive(Transformers = new[] { typeof(RemoveNullConditionalPatterns) })]
public string? CustomerName => Customer?.Name;
// Block body (opt-in)
[Expressive(AllowBlockBody = true)]
public string GetCategory()
{
var threshold = Quantity * 10;
if (threshold > 100) return "Bulk";
return "Regular";
}
}
// Extension methods must be in a static class
public static class OrderExtensions
{
[Expressive]
public static string? SafeTag(this Order o) => o.Tag ?? "N/A";
}
public class OrderSummaryDto
{
public int Id { get; set; }
public string Description { get; set; } = "";
public double Total { get; set; }
public OrderSummaryDto() { }
// Constructor projection -- translates to SQL MemberInit
[Expressive]
public OrderSummaryDto(int id, string description, double total)
{
Id = id;
Description = description;
Total = total;
}
}Usage in an EF Core query:
var results = db.Orders
.AsExpressiveDbSet()
.Where(o => o.Customer?.Email != null)
.Select(o => new OrderSummaryDto(o.Id, o.Tag ?? "N/A", o.Total))
.ToList();Generated SQL:
SELECT "o"."Id",
COALESCE("o"."Tag", 'N/A') AS "Description",
"o"."Price" * CAST("o"."Quantity" AS REAL) AS "Total"
FROM "Orders" AS "o"
LEFT JOIN "Customers" AS "c" ON "o"."CustomerId" = "c"."Id"
WHERE "c"."Email" IS NOT NULL