diff --git a/.editorconfig b/.editorconfig
index 30882e46..5affb9be 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,6 +8,7 @@
#use soft tabs (spaces) for indentation
indent_style = space
+indent_size = 4
#Formatting - new line options
@@ -131,3 +132,7 @@ dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
+
+[*.{csproj,props,targets}]
+indent_style = space
+indent_size = 2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a5b1eb71..bc526d3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -370,3 +370,4 @@ FodyWeavers.xsd
# Docs
/docs/.vitepress/cache/
/docs/.vitepress/dist/
+/ReadmeSample.db
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ff81c4d0..26fd55d1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -21,6 +21,7 @@
+
diff --git a/samples/ReadmeSample/ApplicationDbContext.cs b/samples/ReadmeSample/ApplicationDbContext.cs
index 56ee87bb..1d3b5229 100644
--- a/samples/ReadmeSample/ApplicationDbContext.cs
+++ b/samples/ReadmeSample/ApplicationDbContext.cs
@@ -1,31 +1,28 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Threading.Tasks;
-using EntityFrameworkCore.Projectables.Extensions;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
using ReadmeSample.Entities;
-namespace ReadmeSample
+namespace ReadmeSample;
+
+public class ApplicationDbContext : DbContext
{
- public class ApplicationDbContext : DbContext
- {
- public DbSet Users { get; set; }
- public DbSet Products { get; set; }
- public DbSet Orders { get; set; }
+ public DbSet Users => Set();
+ public DbSet Products => Set();
+ public DbSet Orders => Set();
+ public DbSet Suppliers => Set();
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- {
- optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ReadmeSample;Trusted_Connection=True");
- optionsBuilder.UseProjectables();
- }
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ReadmeSample;Trusted_Connection=True");
- protected override void OnModelCreating(ModelBuilder modelBuilder)
- {
- modelBuilder.Entity().HasKey(x => new { x.OrderId, x.ProductId });
- }
+ // Feature 10: Compatibility mode
+ // Full (default) — expands every query on each invocation; maximum compatibility.
+ // Limited — expands once, then caches; better performance for repeated queries.
+ // Switch with: optionsBuilder.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited));
+ optionsBuilder.UseProjectables();
+ }
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity().HasKey(x => new { x.OrderId, x.ProductId });
}
}
diff --git a/samples/ReadmeSample/ConsoleHelper.cs b/samples/ReadmeSample/ConsoleHelper.cs
new file mode 100644
index 00000000..e57153bb
--- /dev/null
+++ b/samples/ReadmeSample/ConsoleHelper.cs
@@ -0,0 +1,59 @@
+using System.Text.RegularExpressions;
+using Spectre.Console;
+
+namespace ReadmeSample;
+
+/// Spectre.Console rendering helpers used by Program.cs.
+static internal partial class ConsoleHelper
+{
+ ///
+ /// Applies Spectre.Console markup to a raw SQL string:
+ /// string literals → green, SQL keywords → bold cyan, ef_* helpers → dim grey.
+ ///
+ private static string SqlMarkup(string sql)
+ {
+ // Escape [ and ] so Spectre doesn't misinterpret them as markup tags.
+ var esc = Markup.Escape(sql);
+
+ // One-pass regex — order of alternatives matters:
+ // group 1 → single-quoted string literals
+ // group 2 → multi-word keywords (INNER JOIN, LEFT JOIN, ORDER BY, GROUP BY)
+ // group 3 → single-word SQL keywords
+ // group 4 → SQLite-specific ef_* helper functions
+ return SqlHighlightRegex().Replace(esc, m =>
+ {
+ if (m.Groups[1].Success) return $"[green]{m.Value}[/]";
+ if (m.Groups[2].Success || m.Groups[3].Success) return $"[bold deepskyblue1]{m.Value}[/]";
+ if (m.Groups[4].Success) return $"[grey50]{m.Value}[/]";
+ return m.Value;
+ });
+ }
+
+ /// Renders a numbered feature section header using a yellow rule.
+ public static void Section(int n, string title)
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(
+ new Rule($"[bold yellow]Feature {n}[/] — [white]{Markup.Escape(title)}[/]")
+ .LeftJustified()
+ .RuleStyle("dim yellow"));
+ AnsiConsole.WriteLine();
+ }
+
+ /// Renders a SQL string inside a rounded panel with syntax highlighting.
+ public static void ShowSql(string sql)
+ {
+ AnsiConsole.Write(new Panel(new Markup(SqlMarkup(sql)))
+ {
+ Header = new PanelHeader("[grey50] SQL [/]"),
+ Border = BoxBorder.Rounded,
+ BorderStyle = Style.Parse("grey"),
+ Padding = new Padding(1, 0, 1, 0),
+ });
+ AnsiConsole.WriteLine();
+ }
+
+ [GeneratedRegex(@"('[^']*')|(\bINNER JOIN\b|\bLEFT JOIN\b|\bORDER BY\b|\bGROUP BY\b)|(\bSELECT\b|\bFROM\b|\bWHERE\b|\bCASE\b|\bWHEN\b|\bTHEN\b|\bELSE\b|\bEND\b|\bAND\b|\bOR\b|\bNOT\b|\bNULL\b|\bIS\b|\bIN\b|\bON\b|\bAS\b|\bLIMIT\b|\bCOALESCE\b|\bCAST\b)|(\bef_\w+\b)", RegexOptions.IgnoreCase, matchTimeoutMilliseconds: 500)]
+ private static partial Regex SqlHighlightRegex();
+}
+
diff --git a/samples/ReadmeSample/Dtos/OrderSummaryDto.cs b/samples/ReadmeSample/Dtos/OrderSummaryDto.cs
new file mode 100644
index 00000000..8b720f79
--- /dev/null
+++ b/samples/ReadmeSample/Dtos/OrderSummaryDto.cs
@@ -0,0 +1,30 @@
+using EntityFrameworkCore.Projectables;
+using ReadmeSample.Entities;
+
+namespace ReadmeSample.Dtos;
+
+///
+/// DTO with a [Projectable] constructor — the entire mapping is inlined into SQL by the source generator.
+///
+public class OrderSummaryDto
+{
+ public int Id { get; set; }
+ public string? UserName { get; set; }
+ public decimal GrandTotal { get; set; }
+ public string? StatusName { get; set; }
+ public string? PriorityLabel { get; set; }
+
+ /// Required parameterless constructor (EFP0008 ensures its presence).
+ public OrderSummaryDto() { }
+
+ [Projectable]
+ public OrderSummaryDto(Order order)
+ {
+ Id = order.Id;
+ UserName = order.User.UserName;
+ GrandTotal = order.GrandTotal;
+ StatusName = order.StatusDisplayName;
+ PriorityLabel = order.PriorityLabel;
+ }
+}
+
diff --git a/samples/ReadmeSample/Entities/Order.cs b/samples/ReadmeSample/Entities/Order.cs
index 19c5f4ee..a8c7510e 100644
--- a/samples/ReadmeSample/Entities/Order.cs
+++ b/samples/ReadmeSample/Entities/Order.cs
@@ -1,26 +1,92 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using EntityFrameworkCore.Projectables;
-
-namespace ReadmeSample.Entities
+using EntityFrameworkCore.Projectables;
+
+namespace ReadmeSample.Entities;
+
+public class Order
{
- public class Order
- {
- public int Id { get; set; }
- public int UserId { get; set; }
- public DateTime CreatedDate { get; set; }
- public DateTime? FulfilledDate { get; set; }
+ public int Id { get; set; }
+ public int UserId { get; set; }
+ public DateTime CreatedDate { get; set; }
+ public DateTime? FulfilledDate { get; set; }
+ public decimal TaxRate { get; set; }
+ public OrderStatus Status { get; set; }
+
+ public User User { get; set; } = null!;
+ public ICollection Items { get; set; } = [];
+
+ // ── Feature 1: Projectable properties (compose each other recursively) ──────
+
+ /// Sum of (unit price × quantity) for all items — inlined into SQL.
+ [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
+
+ /// Tax amount (Subtotal × TaxRate) — composed from another [Projectable].
+ [Projectable] public decimal Tax => Subtotal * TaxRate;
+
+ /// Total including tax — composed from two [Projectable] properties.
+ [Projectable] public decimal GrandTotal => Subtotal + Tax;
+
+ /// True when the order has been fulfilled — usable in .Where() filters.
+ [Projectable] public bool IsFulfilled => FulfilledDate != null;
+
+ // ── Feature 1 (method): Projectable method with a parameter ─────────────────
- public decimal TaxRate { get; set; }
+ /// Grand total after applying a percentage discount — demonstrates a [Projectable] method.
+ [Projectable]
+ public decimal GetDiscountedTotal(decimal discountPct) => GrandTotal * (1 - discountPct);
- public User User { get; set; }
- public ICollection Items { get; set; }
+ // ── Feature 5: Pattern matching — switch expression ──────────────────────────
- [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
- [Projectable] public decimal Tax => Subtotal * TaxRate;
- [Projectable] public decimal GrandTotal => Subtotal * TaxRate;
+ ///
+ /// Priority label derived from GrandTotal using a switch expression.
+ /// The generator rewrites this into SQL CASE WHEN expressions.
+ ///
+ [Projectable]
+ public string PriorityLabel => GrandTotal switch
+ {
+ >= 100m => "High",
+ >= 30m => "Medium",
+ _ => "Low",
+ };
+
+ // ── Feature 6: Block-bodied member (experimental) ────────────────────────────
+
+ ///
+ /// Shipping category determined via an if/else block body.
+ /// AllowBlockBody = true acknowledges the experimental nature (suppresses EFP0001).
+ /// The block is converted to a ternary expression — identical SQL to the switch above.
+ ///
+ [Projectable(AllowBlockBody = true)]
+ public string GetShippingCategory()
+ {
+ if (GrandTotal >= 100m)
+ return "Express";
+ else if (GrandTotal >= 30m)
+ return "Standard";
+ else
+ return "Economy";
}
+
+ // ── Feature 8: Enum method expansion ─────────────────────────────────────────
+
+ ///
+ /// Human-readable status label.
+ /// ExpandEnumMethods = true makes the generator enumerate every OrderStatus value at
+ /// compile time and bake the results in as a SQL CASE expression — the GetDisplayName()
+ /// method itself never runs at runtime.
+ ///
+ [Projectable(ExpandEnumMethods = true)]
+ public string StatusDisplayName => Status.GetDisplayName();
+
+ // ── Feature 9: UseMemberBody ──────────────────────────────────────────────────
+
+ // Private EF-compatible expression — the actual body EF Core will use.
+ private bool IsHighValueOrderImpl => GrandTotal >= 50m;
+
+ ///
+ /// UseMemberBody delegates the expression source to IsHighValueOrderImpl.
+ /// The annotated member's own body is ignored by the generator; the target
+ /// member's body is used as the expression tree instead.
+ ///
+ [Projectable(UseMemberBody = nameof(IsHighValueOrderImpl))]
+ public bool IsHighValueOrder => IsHighValueOrderImpl;
}
diff --git a/samples/ReadmeSample/Entities/OrderItem.cs b/samples/ReadmeSample/Entities/OrderItem.cs
index a74b9455..5bbf8f31 100644
--- a/samples/ReadmeSample/Entities/OrderItem.cs
+++ b/samples/ReadmeSample/Entities/OrderItem.cs
@@ -1,12 +1,11 @@
-namespace ReadmeSample.Entities
+namespace ReadmeSample.Entities;
+
+public class OrderItem
{
- public class OrderItem
- {
- public int OrderId { get; set; }
- public int ProductId { get; set; }
- public int Quantity { get; set; }
+ public int OrderId { get; set; }
+ public int ProductId { get; set; }
+ public int Quantity { get; set; }
- public Order Order { get; set; }
- public Product Product { get; set; }
- }
+ public Order Order { get; set; } = null!;
+ public Product Product { get; set; } = null!;
}
diff --git a/samples/ReadmeSample/Entities/OrderStatus.cs b/samples/ReadmeSample/Entities/OrderStatus.cs
new file mode 100644
index 00000000..1dcebe81
--- /dev/null
+++ b/samples/ReadmeSample/Entities/OrderStatus.cs
@@ -0,0 +1,26 @@
+namespace ReadmeSample.Entities;
+
+public enum OrderStatus
+{
+ Pending,
+ Fulfilled,
+ Cancelled,
+}
+
+public static class OrderStatusExtensions
+{
+ ///
+ /// Plain C# method — not [Projectable]. Used with ExpandEnumMethods = true.
+ /// The generator evaluates this at compile time for every enum value and bakes
+ /// the results into a CASE expression EF Core can translate to SQL.
+ ///
+ public static string GetDisplayName(this OrderStatus status) =>
+ status switch
+ {
+ OrderStatus.Pending => "Pending Review",
+ OrderStatus.Fulfilled => "Fulfilled",
+ OrderStatus.Cancelled => "Cancelled",
+ _ => status.ToString(),
+ };
+}
+
diff --git a/samples/ReadmeSample/Entities/Product.cs b/samples/ReadmeSample/Entities/Product.cs
index 488213b0..4d98f573 100644
--- a/samples/ReadmeSample/Entities/Product.cs
+++ b/samples/ReadmeSample/Entities/Product.cs
@@ -1,11 +1,21 @@
-namespace ReadmeSample.Entities
+using EntityFrameworkCore.Projectables;
+
+namespace ReadmeSample.Entities;
+
+public class Product
{
- public class Product
- {
- public int Id { get; set; }
+ public int Id { get; set; }
+ public required string Name { get; set; }
+ public decimal ListPrice { get; set; }
- public string Name { get; set; }
+ // Optional supplier — foreign key is nullable so the join is a LEFT JOIN in SQL.
+ public int? SupplierId { get; set; }
+ public Supplier? Supplier { get; set; }
- public decimal ListPrice { get; set; }
- }
+ ///
+ /// Null-conditional rewriting (NullConditionalRewriteSupport.Ignore):
+ /// the ?. operator is stripped and EF Core handles nullability via the LEFT JOIN.
+ ///
+ [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+ public string? SupplierName => Supplier?.Name;
}
diff --git a/samples/ReadmeSample/Entities/Supplier.cs b/samples/ReadmeSample/Entities/Supplier.cs
new file mode 100644
index 00000000..4b5a61d7
--- /dev/null
+++ b/samples/ReadmeSample/Entities/Supplier.cs
@@ -0,0 +1,11 @@
+namespace ReadmeSample.Entities;
+
+/// Optional supplier linked to a product — used to demonstrate null-conditional rewriting.
+public class Supplier
+{
+ public int Id { get; set; }
+ public required string Name { get; set; }
+
+ public ICollection Products { get; set; } = [];
+}
+
diff --git a/samples/ReadmeSample/Entities/User.cs b/samples/ReadmeSample/Entities/User.cs
index 596c161f..74e374f2 100644
--- a/samples/ReadmeSample/Entities/User.cs
+++ b/samples/ReadmeSample/Entities/User.cs
@@ -1,16 +1,10 @@
-using System.Collections;
-using System.Collections.Generic;
+namespace ReadmeSample.Entities;
-namespace ReadmeSample.Entities
+public class User
{
- public class User
- {
- public int Id { get; set; }
+ public int Id { get; set; }
+ public required string UserName { get; set; }
+ public required string EmailAddress { get; set; }
- public string UserName { get; set; }
-
- public string EmailAddress { get; set; }
-
- public ICollection Orders { get; set; }
- }
+ public ICollection Orders { get; set; } = [];
}
diff --git a/samples/ReadmeSample/Extensions/UserExtensions.cs b/samples/ReadmeSample/Extensions/UserExtensions.cs
index 80437a51..56bad7da 100644
--- a/samples/ReadmeSample/Extensions/UserExtensions.cs
+++ b/samples/ReadmeSample/Extensions/UserExtensions.cs
@@ -1,20 +1,28 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using EntityFrameworkCore.Projectables;
+using EntityFrameworkCore.Projectables;
using ReadmeSample.Entities;
-namespace ReadmeSample.Extensions
+namespace ReadmeSample.Extensions;
+
+public static class UserExtensions
{
- public static class UserExtensions
- {
- [Projectable]
- public static Order GetMostRecentOrderForUser(this User user, bool includeUnfulfilled) =>
- user.Orders
- .Where(x => !includeUnfulfilled ? x .FulfilledDate != null : true)
- .OrderByDescending(x => x.CreatedDate)
- .FirstOrDefault();
- }
+ ///
+ /// Returns the most recent order for the user.
+ /// Matches the README example — inlined into SQL via [Projectable].
+ ///
+ [Projectable]
+ public static Order? GetMostRecentOrder(this User user) =>
+ user.Orders
+ .OrderByDescending(x => x.CreatedDate)
+ .FirstOrDefault();
+
+ ///
+ /// Returns the most recent order with an optional filter on fulfillment status.
+ /// Demonstrates method overloads: both variants are supported.
+ ///
+ [Projectable]
+ public static Order? GetMostRecentOrderForUser(this User user, bool includeUnfulfilled) =>
+ user.Orders
+ .Where(x => includeUnfulfilled || x.FulfilledDate != null)
+ .OrderByDescending(x => x.CreatedDate)
+ .FirstOrDefault();
}
diff --git a/samples/ReadmeSample/Program.cs b/samples/ReadmeSample/Program.cs
index ca242705..46b2c447 100644
--- a/samples/ReadmeSample/Program.cs
+++ b/samples/ReadmeSample/Program.cs
@@ -1,35 +1,171 @@
-using ReadmeSample;
-using ReadmeSample.Entities;
+using Microsoft.EntityFrameworkCore;
+using ReadmeSample;
+using ReadmeSample.Dtos;
using ReadmeSample.Extensions;
+using Spectre.Console;
+using static ReadmeSample.ConsoleHelper;
-using var dbContext = new ApplicationDbContext();
-
-// recreate database
-dbContext.Database.EnsureDeleted();
-dbContext.Database.EnsureCreated();
-
-// Populate with seed data
-var sampleUser = new User { UserName = "Jon", EmailAddress = "jon@doe.com" };
-var sampleProduct = new Product { Name = "Blue Pen", ListPrice = 1.5m };
-var sampleOrder = new Order {
- User = sampleUser,
- TaxRate = .19m,
- CreatedDate = DateTime.UtcNow.AddDays(-1),
- FulfilledDate = DateTime.UtcNow,
- Items = new List {
- new OrderItem { Product = sampleProduct, Quantity = 5 }
- }
-};
-
-dbContext.AddRange(sampleUser, sampleProduct, sampleOrder);
-dbContext.SaveChanges();
-
-var query = dbContext.Users
- .Where(x => x.UserName == sampleUser.UserName)
- .Select(x => new {
- GrandTotal = x.GetMostRecentOrderForUser(/* includeUnfulfilled: */ false).GrandTotal
- });
-
-var result = query.First();
-
-Console.WriteLine($"Jons latest order had a grant total of {result.GrandTotal}");
+// ─────────────────────────────────────────────────────────────────────────────
+// Banner
+// ─────────────────────────────────────────────────────────────────────────────
+AnsiConsole.Write(new Panel(
+ "[bold yellow]EntityFrameworkCore.Projectables[/] — Feature Tour\n"
+ + "[dim]SQL Server · .NET 10 · All 11 features from the README demonstrated[/]")
+{
+ Border = BoxBorder.Double,
+ BorderStyle = Style.Parse("yellow dim"),
+ Padding = new Padding(2, 0, 2, 0),
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Bootstrap
+// ─────────────────────────────────────────────────────────────────────────────
+await using var dbContext = new ApplicationDbContext();
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 1 — Properties & methods
+// ─────────────────────────────────────────────────────────────────────────────
+Section(1, "Properties & methods");
+
+var totalsQuery = dbContext.Orders.Select(o => new
+{
+ o.Id, o.Subtotal, o.Tax, o.GrandTotal,
+ Discounted10Pct = o.GetDiscountedTotal(0.10m),
+});
+
+ShowSql(totalsQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 2 — Extension methods
+// ─────────────────────────────────────────────────────────────────────────────
+Section(2, "Extension methods");
+
+var recentQuery = dbContext.Users
+ .Where(u => u.UserName == "Jon")
+ .Select(u => new { u.UserName, LatestOrderGrandTotal = u.GetMostRecentOrder()!.GrandTotal });
+
+ShowSql(recentQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 3 — Constructor projections
+// ─────────────────────────────────────────────────────────────────────────────
+Section(3, "Constructor projections → new OrderSummaryDto(o)");
+
+var dtoQuery = dbContext.Orders.Select(o => new OrderSummaryDto(o));
+
+ShowSql(dtoQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 4 — Method overloads
+// ─────────────────────────────────────────────────────────────────────────────
+Section(4, "Method overloads");
+
+var withPendingQuery = dbContext.Users
+ .Where(u => u.UserName == "Jon")
+ .Select(u => new { u.UserName, LatestAnyOrderTotal = u.GetMostRecentOrderForUser(true)!.GrandTotal });
+
+ShowSql(withPendingQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 5 — Pattern matching
+// ─────────────────────────────────────────────────────────────────────────────
+Section(5, "Pattern matching → switch expression becomes SQL CASE WHEN");
+
+var priorityQuery = dbContext.Orders.Select(o => new { o.Id, o.GrandTotal, o.PriorityLabel });
+
+ShowSql(priorityQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 6 — Block-bodied members
+// ─────────────────────────────────────────────────────────────────────────────
+Section(6, "Block-bodied members → AllowBlockBody = true");
+
+var shippingQuery = dbContext.Orders.Select(o => new { o.Id, ShippingCategory = o.GetShippingCategory() });
+
+ShowSql(shippingQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 7 — Null-conditional rewriting
+// ─────────────────────────────────────────────────────────────────────────────
+Section(7, "Null-conditional rewriting → NullConditionalRewriteSupport.Ignore");
+
+var supplierQuery = dbContext.Products.Select(p => new { p.Name, p.SupplierName });
+
+ShowSql(supplierQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 8 — Enum method expansion
+// ─────────────────────────────────────────────────────────────────────────────
+Section(8, "Enum method expansion → ExpandEnumMethods = true");
+
+var statusQuery = dbContext.Orders.Select(o => new { o.Id, o.Status, o.StatusDisplayName });
+
+ShowSql(statusQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 9 — UseMemberBody
+// ─────────────────────────────────────────────────────────────────────────────
+Section(9, "UseMemberBody → expression sourced from a private member");
+
+var highValueQuery = dbContext.Orders.Select(o => new { o.Id, o.GrandTotal, o.IsHighValueOrder });
+
+ShowSql(highValueQuery.ToQueryString());
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 10 — Compatibility mode
+// ─────────────────────────────────────────────────────────────────────────────
+Section(10, "Compatibility mode → UseProjectables(p => p.CompatibilityMode(...))");
+
+var modeTable = new Table { Border = TableBorder.Rounded, BorderStyle = Style.Parse("grey") };
+modeTable.AddColumn(new TableColumn("[bold]Mode[/]"));
+modeTable.AddColumn(new TableColumn("[bold]When expansion runs[/]"));
+modeTable.AddColumn(new TableColumn("[bold]Query cache[/]"));
+modeTable.AddColumn(new TableColumn("[bold]Performance[/]"));
+modeTable.AddRow(
+ "[bold]Full[/] [dim](default)[/]",
+ "Every query invocation",
+ "Per query shape",
+ "Baseline");
+modeTable.AddRow(
+ "[bold]Limited[/]",
+ "First invocation, then cached",
+ "[bold green]Reused[/]",
+ "[bold chartreuse1]Often faster than vanilla EF[/]");
+AnsiConsole.Write(modeTable);
+AnsiConsole.WriteLine();
+AnsiConsole.MarkupLine(" Configure in [cyan]ApplicationDbContext.OnConfiguring[/]:");
+AnsiConsole.MarkupLine(" [dim]// Full (default)[/]");
+AnsiConsole.MarkupLine(" [cyan]optionsBuilder.UseProjectables()[/]");
+AnsiConsole.MarkupLine(" [dim]// Limited[/]");
+AnsiConsole.MarkupLine(" [cyan]optionsBuilder.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited))[/]");
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Feature 11 — Roslyn analyzers & code fixes
+// ─────────────────────────────────────────────────────────────────────────────
+Section(11, "Roslyn analyzers & code fixes → EFP0001–EFP0012 (compile-time)");
+
+var diagTable = new Table { Border = TableBorder.Rounded, BorderStyle = Style.Parse("grey") };
+diagTable.AddColumn(new TableColumn("[bold]Code[/]").Centered());
+diagTable.AddColumn(new TableColumn("[bold]Triggered when…[/]"));
+diagTable.AddColumn(new TableColumn("[bold]IDE quick-fix[/]"));
+diagTable.AddRow(
+ "[bold yellow]EFP0001[/]",
+ "Block body without [cyan]AllowBlockBody = true[/]",
+ "Add [cyan]AllowBlockBody = true[/]");
+diagTable.AddRow(
+ "[bold red1]EFP0002[/]",
+ "[cyan]?.[/] used without [cyan]NullConditionalRewriteSupport[/]",
+ "Choose [cyan]Ignore[/] or [cyan]Rewrite[/]");
+diagTable.AddRow(
+ "[bold red1]EFP0008[/]",
+ "DTO class missing parameterless constructor",
+ "Insert parameterless constructor");
+diagTable.AddRow(
+ "[bold blue]EFP0012[/]",
+ "Factory method can be converted to a [cyan][[Projectable]][/] constructor",
+ "Convert & update call sites");
+AnsiConsole.Write(diagTable);
+AnsiConsole.WriteLine();
+AnsiConsole.MarkupLine(
+ " [dim]Diagnostics are reported at compile time in the IDE — see source comments for examples.[/]");
+AnsiConsole.WriteLine();
diff --git a/samples/ReadmeSample/README.md b/samples/ReadmeSample/README.md
new file mode 100644
index 00000000..d5fe4176
--- /dev/null
+++ b/samples/ReadmeSample/README.md
@@ -0,0 +1,258 @@
+# ReadmeSample
+
+A runnable sample illustrating **every feature** from the [Features table](../../README.md) of EntityFrameworkCore.Projectables, using a **local SQLite database** created automatically on startup.
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- No database server required — SQLite is embedded.
+
+## Running the sample
+
+```bash
+dotnet run --project samples/ReadmeSample
+```
+
+The `ReadmeSample.db` file is recreated automatically on every run (`EnsureDeleted` / `EnsureCreated`).
+Each section prints the generated SQL followed by the query results.
+
+---
+
+## Project structure
+
+```
+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
+```
+
+---
+
+## Features demonstrated
+
+All features from the [root README features table](../../README.md#features-v6x) are covered.
+
+### Feature 1 — Properties & methods
+
+**Properties** compose each other recursively — `GrandTotal` inlines `Subtotal` and `Tax`:
+
+```csharp
+[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:
+
+```csharp
+[Projectable]
+public decimal GetDiscountedTotal(decimal discountPct) => GrandTotal * (1 - discountPct);
+```
+
+### Feature 2 — Extension methods
+
+The extension method body is inlined as a correlated subquery:
+
+```csharp
+// Extensions/UserExtensions.cs
+[Projectable]
+public static Order? GetMostRecentOrder(this User user) =>
+ user.Orders.OrderByDescending(x => x.CreatedDate).FirstOrDefault();
+```
+
+### Feature 3 — Constructor projections
+
+Mark a constructor with `[Projectable]` to project a DTO entirely in SQL — no client-side mapping:
+
+```csharp
+// 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));
+```
+
+### Feature 4 — Method overloads
+
+Both overloads of `GetMostRecentOrderForUser` are independently supported; each generates its own expression class:
+
+```csharp
+[Projectable]
+public static Order? GetMostRecentOrder(this User user) => …;
+
+[Projectable]
+public static Order? GetMostRecentOrderForUser(this User user, bool includeUnfulfilled) => …;
+```
+
+### Feature 5 — Pattern matching (`switch`, `is`)
+
+Switch expressions are rewritten into SQL `CASE WHEN` expressions:
+
+```csharp
+[Projectable]
+public string PriorityLabel => GrandTotal switch
+{
+ >= 100m => "High",
+ >= 30m => "Medium",
+ _ => "Low",
+};
+```
+
+Generated SQL:
+```sql
+CASE WHEN GrandTotal >= 100 THEN 'High'
+ WHEN GrandTotal >= 30 THEN 'Medium'
+ ELSE 'Low' END
+```
+
+### Feature 6 — Block-bodied members (experimental)
+
+`if`/`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**:
+
+```csharp
+[Projectable(AllowBlockBody = true)]
+public string GetShippingCategory()
+{
+ if (GrandTotal >= 100m)
+ return "Express";
+ else if (GrandTotal >= 30m)
+ return "Standard";
+ else
+ return "Economy";
+}
+```
+
+### Feature 7 — Null-conditional rewriting
+
+`Supplier?.Name` uses the null-conditional operator, which cannot be expressed in an `Expression` directly.
+`NullConditionalRewriteSupport.Ignore` strips the `?.` — EF Core handles nullability via a `LEFT JOIN`:
+
+```csharp
+// Entities/Product.cs
+[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
+public string? SupplierName => Supplier?.Name;
+```
+
+Generated SQL:
+```sql
+SELECT p.Name, s.Name AS SupplierName
+FROM Products p
+LEFT JOIN Suppliers s ON p.SupplierId = s.Id
+```
+
+> Use `NullConditionalRewriteSupport.Rewrite` for explicit `CASE WHEN NULL` guards (safer for Cosmos DB).
+
+### Feature 8 — Enum method expansion
+
+`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:
+
+```csharp
+// 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:
+```sql
+CASE WHEN Status = 0 THEN 'Pending Review'
+ WHEN Status = 1 THEN 'Fulfilled'
+ WHEN Status = 2 THEN 'Cancelled' END
+```
+
+### Feature 9 — `UseMemberBody`
+
+`UseMemberBody` 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:
+
+```csharp
+// 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;
+```
+
+### Feature 10 — Compatibility mode
+
+Configured in `ApplicationDbContext.OnConfiguring`:
+
+```csharp
+// 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 |
+
+### Feature 11 — Roslyn analyzers & code fixes (EFP0001–EFP0012)
+
+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](https://efnext.github.io/reference/diagnostics) for the full list.
+
+---
+
+## How it works
+
+1. The **Roslyn Source Generator** (`EntityFrameworkCore.Projectables.Generator`) inspects every `[Projectable]`-annotated member at compile time and emits a companion `Expression` property.
+2. 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.
+
+---
+
+## Environment
+
+| 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 |
diff --git a/samples/ReadmeSample/ReadmeSample.csproj b/samples/ReadmeSample/ReadmeSample.csproj
index 72664f6c..25df9fc2 100644
--- a/samples/ReadmeSample/ReadmeSample.csproj
+++ b/samples/ReadmeSample/ReadmeSample.csproj
@@ -1,14 +1,14 @@
- Exe
- disable
- false
+ Exe
+ net10.0;
+ false
-
-
+
+