A runnable sample illustrating every feature from the Features table of EntityFrameworkCore.Projectables, using a local SQLite database created automatically on startup.
- .NET 10 SDK
- No database server required — SQLite is embedded.
dotnet run --project samples/ReadmeSampleThe ReadmeSample.db file is recreated automatically on every run (EnsureDeleted / EnsureCreated).
Each section prints the generated SQL followed by the query results.
ReadmeSample/
├── Program.cs # Entry point — 11 numbered feature demos
├── ApplicationDbContext.cs # SQLite DbContext with UseProjectables()
├── Entities/
│ ├── User.cs # User with an Orders collection
│ ├── Order.cs # Order entity — most features live here
│ ├── OrderItem.cs # Order line item (composite primary key)
│ ├── Product.cs # Product with optional Supplier navigation
│ ├── Supplier.cs # Optional supplier (for null-conditional demo)
│ └── OrderStatus.cs # Enum + GetDisplayName() (for enum expansion demo)
├── Dtos/
│ └── OrderSummaryDto.cs # DTO with a [Projectable] constructor
└── Extensions/
└── UserExtensions.cs # [Projectable] extension methods on User
All features from the root README features table are covered.
Properties compose each other recursively — GrandTotal inlines Subtotal and Tax:
[Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
[Projectable] public decimal Tax => Subtotal * TaxRate;
[Projectable] public decimal GrandTotal => Subtotal + Tax;Methods accept parameters and are equally inlined into SQL:
[Projectable]
public decimal GetDiscountedTotal(decimal discountPct) => GrandTotal * (1 - discountPct);The extension method body is inlined as a correlated subquery:
// Extensions/UserExtensions.cs
[Projectable]
public static Order? GetMostRecentOrder(this User user) =>
user.Orders.OrderByDescending(x => x.CreatedDate).FirstOrDefault();Mark a constructor with [Projectable] to project a DTO entirely in SQL — no client-side mapping:
// Dtos/OrderSummaryDto.cs
public OrderSummaryDto() { } // required parameterless ctor (EFP0008 ensures its presence)
[Projectable]
public OrderSummaryDto(Order order)
{
Id = order.Id;
UserName = order.User.UserName;
GrandTotal = order.GrandTotal; // other [Projectable] members are recursively inlined
StatusName = order.StatusDisplayName;
PriorityLabel = order.PriorityLabel;
}
// Usage
dbContext.Orders.Select(o => new OrderSummaryDto(o));Both overloads of GetMostRecentOrderForUser are independently supported; each generates its own expression class:
[Projectable]
public static Order? GetMostRecentOrder(this User user) => …;
[Projectable]
public static Order? GetMostRecentOrderForUser(this User user, bool includeUnfulfilled) => …;Switch expressions are rewritten into SQL CASE WHEN expressions:
[Projectable]
public string PriorityLabel => GrandTotal switch
{
>= 100m => "High",
>= 30m => "Medium",
_ => "Low",
};Generated SQL:
CASE WHEN GrandTotal >= 100 THEN 'High'
WHEN GrandTotal >= 30 THEN 'Medium'
ELSE 'Low' ENDif/else block bodies are converted to ternary expressions, producing identical SQL to a switch expression.
AllowBlockBody = true acknowledges the experimental nature and suppresses warning EFP0001:
[Projectable(AllowBlockBody = true)]
public string GetShippingCategory()
{
if (GrandTotal >= 100m)
return "Express";
else if (GrandTotal >= 30m)
return "Standard";
else
return "Economy";
}Supplier?.Name uses the null-conditional operator, which cannot be expressed in an Expression<T> directly.
NullConditionalRewriteSupport.Ignore strips the ?. — EF Core handles nullability via a LEFT JOIN:
// Entities/Product.cs
[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
public string? SupplierName => Supplier?.Name;Generated SQL:
SELECT p.Name, s.Name AS SupplierName
FROM Products p
LEFT JOIN Suppliers s ON p.SupplierId = s.IdUse
NullConditionalRewriteSupport.Rewritefor explicitCASE WHEN NULLguards (safer for Cosmos DB).
GetDisplayName() is a plain C# method — not [Projectable]. With ExpandEnumMethods = true, the generator
evaluates it at compile time for every enum value and bakes the results into a SQL CASE expression.
The method never runs at query time:
// Entities/OrderStatus.cs
public static string GetDisplayName(this OrderStatus status) => status switch
{
OrderStatus.Pending => "Pending Review",
OrderStatus.Fulfilled => "Fulfilled",
OrderStatus.Cancelled => "Cancelled",
_ => status.ToString(),
};
// Entities/Order.cs
[Projectable(ExpandEnumMethods = true)]
public string StatusDisplayName => Status.GetDisplayName();Generated SQL:
CASE WHEN Status = 0 THEN 'Pending Review'
WHEN Status = 1 THEN 'Fulfilled'
WHEN Status = 2 THEN 'Cancelled' ENDUseMemberBody replaces the annotated member's expression source with another member's body.
Useful when the public member has a different in-memory implementation but you want a clean SQL expression:
// Private EF-compatible expression
private bool IsHighValueOrderImpl => GrandTotal >= 50m;
// The generator uses IsHighValueOrderImpl's body — the own body is ignored
[Projectable(UseMemberBody = nameof(IsHighValueOrderImpl))]
public bool IsHighValueOrder => IsHighValueOrderImpl;Configured in ApplicationDbContext.OnConfiguring:
// Full (default) — expands every query on each invocation; maximum compatibility
optionsBuilder.UseProjectables();
// Limited — expands once then caches; better performance for repeated queries
optionsBuilder.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited));| Mode | Expansion timing | Query cache | Performance |
|---|---|---|---|
Full |
Every invocation | Per query | Baseline |
Limited |
First invocation, cached | Reused | ✅ Often faster than vanilla EF |
Compile-time only — not demonstrated at runtime. Diagnostics are reported directly in the IDE:
| Code | When triggered | Fix available |
|---|---|---|
EFP0001 |
Block-bodied member without AllowBlockBody = true |
Add AllowBlockBody = true |
EFP0002 |
?. used without configuring NullConditionalRewriteSupport |
Choose Ignore or Rewrite |
EFP0008 |
DTO class missing parameterless constructor | Insert parameterless constructor |
EFP0012 |
Factory method can be a constructor | Convert to [Projectable] ctor |
See the Diagnostics Reference for the full list.
- The Roslyn Source Generator (
EntityFrameworkCore.Projectables.Generator) inspects every[Projectable]-annotated member at compile time and emits a companionExpression<TDelegate>property. - The runtime interceptor (
UseProjectables()) hooks into EF Core's query compilation pipeline and substitutes those expression trees in place of the annotated member calls before SQL translation.
The final SQL contains each member's body inlined directly — no C# method calls at runtime, no client-side evaluation, no N+1.
| Setting | Value |
|---|---|
| .NET TFM | net10.0 |
| C# language | 14.0 |
| Database | SQLite (ReadmeSample.db, local file) |
| EF Core provider | Microsoft.EntityFrameworkCore.Sqlite 10.x |
| Nullable | enabled |