Skip to content

Commit e4b2c4b

Browse files
PhenXfabien.menager
andauthored
Feature/update readme sample (#201)
* Update ReadmeSample project * Better console output * Apply code review suggestions and switch back to SQL server SQL, without query execution, only SQL display * Fix header * Fix header --------- Co-authored-by: fabien.menager <fabien.menager@am-creations.fr>
1 parent fabb896 commit e4b2c4b

16 files changed

Lines changed: 726 additions & 125 deletions

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#use soft tabs (spaces) for indentation
1010
indent_style = space
11+
indent_size = 4
1112

1213
#Formatting - new line options
1314

@@ -131,3 +132,7 @@ dotnet_naming_symbols.instance_fields.applicable_kinds = field
131132

132133
dotnet_naming_style.instance_field_style.capitalization = camel_case
133134
dotnet_naming_style.instance_field_style.required_prefix = _
135+
136+
[*.{csproj,props,targets}]
137+
indent_style = space
138+
indent_size = 2

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,3 +370,4 @@ FodyWeavers.xsd
370370
# Docs
371371
/docs/.vitepress/cache/
372372
/docs/.vitepress/dist/
373+
/ReadmeSample.db

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageVersion Include="Basic.Reference.Assemblies.Net100" Version="1.8.4" />
2222
</ItemGroup>
2323
<ItemGroup>
24+
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
2425
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
2526
<PackageVersion Include="EFCore.BulkExtensions" Version="8.0.4" />
2627
<PackageVersion Include="coverlet.collector" Version="8.0.1" />
Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,28 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Security.Cryptography.X509Certificates;
5-
using System.Text;
6-
using System.Threading.Tasks;
7-
using EntityFrameworkCore.Projectables.Extensions;
8-
using Microsoft.EntityFrameworkCore;
1+
using Microsoft.EntityFrameworkCore;
92
using ReadmeSample.Entities;
103

11-
namespace ReadmeSample
4+
namespace ReadmeSample;
5+
6+
public class ApplicationDbContext : DbContext
127
{
13-
public class ApplicationDbContext : DbContext
14-
{
15-
public DbSet<User> Users { get; set; }
16-
public DbSet<Product> Products { get; set; }
17-
public DbSet<Order> Orders { get; set; }
8+
public DbSet<User> Users => Set<User>();
9+
public DbSet<Product> Products => Set<Product>();
10+
public DbSet<Order> Orders => Set<Order>();
11+
public DbSet<Supplier> Suppliers => Set<Supplier>();
1812

19-
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
20-
{
21-
optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ReadmeSample;Trusted_Connection=True");
22-
optionsBuilder.UseProjectables();
23-
}
13+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
14+
{
15+
optionsBuilder.UseSqlServer("Server=(localdb)\\MSSQLLocalDB;Database=ReadmeSample;Trusted_Connection=True");
2416

25-
protected override void OnModelCreating(ModelBuilder modelBuilder)
26-
{
27-
modelBuilder.Entity<OrderItem>().HasKey(x => new { x.OrderId, x.ProductId });
28-
}
17+
// Feature 10: Compatibility mode
18+
// Full (default) — expands every query on each invocation; maximum compatibility.
19+
// Limited — expands once, then caches; better performance for repeated queries.
20+
// Switch with: optionsBuilder.UseProjectables(p => p.CompatibilityMode(CompatibilityMode.Limited));
21+
optionsBuilder.UseProjectables();
22+
}
2923

24+
protected override void OnModelCreating(ModelBuilder modelBuilder)
25+
{
26+
modelBuilder.Entity<OrderItem>().HasKey(x => new { x.OrderId, x.ProductId });
3027
}
3128
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Text.RegularExpressions;
2+
using Spectre.Console;
3+
4+
namespace ReadmeSample;
5+
6+
/// <summary>Spectre.Console rendering helpers used by Program.cs.</summary>
7+
static internal partial class ConsoleHelper
8+
{
9+
/// <summary>
10+
/// Applies Spectre.Console markup to a raw SQL string:
11+
/// string literals → green, SQL keywords → bold cyan, ef_* helpers → dim grey.
12+
/// </summary>
13+
private static string SqlMarkup(string sql)
14+
{
15+
// Escape [ and ] so Spectre doesn't misinterpret them as markup tags.
16+
var esc = Markup.Escape(sql);
17+
18+
// One-pass regex — order of alternatives matters:
19+
// group 1 → single-quoted string literals
20+
// group 2 → multi-word keywords (INNER JOIN, LEFT JOIN, ORDER BY, GROUP BY)
21+
// group 3 → single-word SQL keywords
22+
// group 4 → SQLite-specific ef_* helper functions
23+
return SqlHighlightRegex().Replace(esc, m =>
24+
{
25+
if (m.Groups[1].Success) return $"[green]{m.Value}[/]";
26+
if (m.Groups[2].Success || m.Groups[3].Success) return $"[bold deepskyblue1]{m.Value}[/]";
27+
if (m.Groups[4].Success) return $"[grey50]{m.Value}[/]";
28+
return m.Value;
29+
});
30+
}
31+
32+
/// <summary>Renders a numbered feature section header using a yellow rule.</summary>
33+
public static void Section(int n, string title)
34+
{
35+
AnsiConsole.WriteLine();
36+
AnsiConsole.Write(
37+
new Rule($"[bold yellow]Feature {n}[/] — [white]{Markup.Escape(title)}[/]")
38+
.LeftJustified()
39+
.RuleStyle("dim yellow"));
40+
AnsiConsole.WriteLine();
41+
}
42+
43+
/// <summary>Renders a SQL string inside a rounded panel with syntax highlighting.</summary>
44+
public static void ShowSql(string sql)
45+
{
46+
AnsiConsole.Write(new Panel(new Markup(SqlMarkup(sql)))
47+
{
48+
Header = new PanelHeader("[grey50] SQL [/]"),
49+
Border = BoxBorder.Rounded,
50+
BorderStyle = Style.Parse("grey"),
51+
Padding = new Padding(1, 0, 1, 0),
52+
});
53+
AnsiConsole.WriteLine();
54+
}
55+
56+
[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)]
57+
private static partial Regex SqlHighlightRegex();
58+
}
59+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using EntityFrameworkCore.Projectables;
2+
using ReadmeSample.Entities;
3+
4+
namespace ReadmeSample.Dtos;
5+
6+
/// <summary>
7+
/// DTO with a [Projectable] constructor — the entire mapping is inlined into SQL by the source generator.
8+
/// </summary>
9+
public class OrderSummaryDto
10+
{
11+
public int Id { get; set; }
12+
public string? UserName { get; set; }
13+
public decimal GrandTotal { get; set; }
14+
public string? StatusName { get; set; }
15+
public string? PriorityLabel { get; set; }
16+
17+
/// <summary>Required parameterless constructor (EFP0008 ensures its presence).</summary>
18+
public OrderSummaryDto() { }
19+
20+
[Projectable]
21+
public OrderSummaryDto(Order order)
22+
{
23+
Id = order.Id;
24+
UserName = order.User.UserName;
25+
GrandTotal = order.GrandTotal;
26+
StatusName = order.StatusDisplayName;
27+
PriorityLabel = order.PriorityLabel;
28+
}
29+
}
30+
Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Text;
5-
using System.Threading.Tasks;
6-
using EntityFrameworkCore.Projectables;
7-
8-
namespace ReadmeSample.Entities
1+
using EntityFrameworkCore.Projectables;
2+
3+
namespace ReadmeSample.Entities;
4+
5+
public class Order
96
{
10-
public class Order
11-
{
12-
public int Id { get; set; }
13-
public int UserId { get; set; }
14-
public DateTime CreatedDate { get; set; }
15-
public DateTime? FulfilledDate { get; set; }
7+
public int Id { get; set; }
8+
public int UserId { get; set; }
9+
public DateTime CreatedDate { get; set; }
10+
public DateTime? FulfilledDate { get; set; }
11+
public decimal TaxRate { get; set; }
12+
public OrderStatus Status { get; set; }
13+
14+
public User User { get; set; } = null!;
15+
public ICollection<OrderItem> Items { get; set; } = [];
16+
17+
// ── Feature 1: Projectable properties (compose each other recursively) ──────
18+
19+
/// <summary>Sum of (unit price × quantity) for all items — inlined into SQL.</summary>
20+
[Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
21+
22+
/// <summary>Tax amount (Subtotal × TaxRate) — composed from another [Projectable].</summary>
23+
[Projectable] public decimal Tax => Subtotal * TaxRate;
24+
25+
/// <summary>Total including tax — composed from two [Projectable] properties.</summary>
26+
[Projectable] public decimal GrandTotal => Subtotal + Tax;
27+
28+
/// <summary>True when the order has been fulfilled — usable in .Where() filters.</summary>
29+
[Projectable] public bool IsFulfilled => FulfilledDate != null;
30+
31+
// ── Feature 1 (method): Projectable method with a parameter ─────────────────
1632

17-
public decimal TaxRate { get; set; }
33+
/// <summary>Grand total after applying a percentage discount — demonstrates a [Projectable] method.</summary>
34+
[Projectable]
35+
public decimal GetDiscountedTotal(decimal discountPct) => GrandTotal * (1 - discountPct);
1836

19-
public User User { get; set; }
20-
public ICollection<OrderItem> Items { get; set; }
37+
// ── Feature 5: Pattern matching — switch expression ──────────────────────────
2138

22-
[Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
23-
[Projectable] public decimal Tax => Subtotal * TaxRate;
24-
[Projectable] public decimal GrandTotal => Subtotal * TaxRate;
39+
/// <summary>
40+
/// Priority label derived from GrandTotal using a switch expression.
41+
/// The generator rewrites this into SQL CASE WHEN expressions.
42+
/// </summary>
43+
[Projectable]
44+
public string PriorityLabel => GrandTotal switch
45+
{
46+
>= 100m => "High",
47+
>= 30m => "Medium",
48+
_ => "Low",
49+
};
50+
51+
// ── Feature 6: Block-bodied member (experimental) ────────────────────────────
52+
53+
/// <summary>
54+
/// Shipping category determined via an if/else block body.
55+
/// AllowBlockBody = true acknowledges the experimental nature (suppresses EFP0001).
56+
/// The block is converted to a ternary expression — identical SQL to the switch above.
57+
/// </summary>
58+
[Projectable(AllowBlockBody = true)]
59+
public string GetShippingCategory()
60+
{
61+
if (GrandTotal >= 100m)
62+
return "Express";
63+
else if (GrandTotal >= 30m)
64+
return "Standard";
65+
else
66+
return "Economy";
2567
}
68+
69+
// ── Feature 8: Enum method expansion ─────────────────────────────────────────
70+
71+
/// <summary>
72+
/// Human-readable status label.
73+
/// ExpandEnumMethods = true makes the generator enumerate every OrderStatus value at
74+
/// compile time and bake the results in as a SQL CASE expression — the GetDisplayName()
75+
/// method itself never runs at runtime.
76+
/// </summary>
77+
[Projectable(ExpandEnumMethods = true)]
78+
public string StatusDisplayName => Status.GetDisplayName();
79+
80+
// ── Feature 9: UseMemberBody ──────────────────────────────────────────────────
81+
82+
// Private EF-compatible expression — the actual body EF Core will use.
83+
private bool IsHighValueOrderImpl => GrandTotal >= 50m;
84+
85+
/// <summary>
86+
/// UseMemberBody delegates the expression source to IsHighValueOrderImpl.
87+
/// The annotated member's own body is ignored by the generator; the target
88+
/// member's body is used as the expression tree instead.
89+
/// </summary>
90+
[Projectable(UseMemberBody = nameof(IsHighValueOrderImpl))]
91+
public bool IsHighValueOrder => IsHighValueOrderImpl;
2692
}
Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
namespace ReadmeSample.Entities
1+
namespace ReadmeSample.Entities;
2+
3+
public class OrderItem
24
{
3-
public class OrderItem
4-
{
5-
public int OrderId { get; set; }
6-
public int ProductId { get; set; }
7-
public int Quantity { get; set; }
5+
public int OrderId { get; set; }
6+
public int ProductId { get; set; }
7+
public int Quantity { get; set; }
88

9-
public Order Order { get; set; }
10-
public Product Product { get; set; }
11-
}
9+
public Order Order { get; set; } = null!;
10+
public Product Product { get; set; } = null!;
1211
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
namespace ReadmeSample.Entities;
2+
3+
public enum OrderStatus
4+
{
5+
Pending,
6+
Fulfilled,
7+
Cancelled,
8+
}
9+
10+
public static class OrderStatusExtensions
11+
{
12+
/// <summary>
13+
/// Plain C# method — not [Projectable]. Used with ExpandEnumMethods = true.
14+
/// The generator evaluates this at compile time for every enum value and bakes
15+
/// the results into a CASE expression EF Core can translate to SQL.
16+
/// </summary>
17+
public static string GetDisplayName(this OrderStatus status) =>
18+
status switch
19+
{
20+
OrderStatus.Pending => "Pending Review",
21+
OrderStatus.Fulfilled => "Fulfilled",
22+
OrderStatus.Cancelled => "Cancelled",
23+
_ => status.ToString(),
24+
};
25+
}
26+
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
namespace ReadmeSample.Entities
1+
using EntityFrameworkCore.Projectables;
2+
3+
namespace ReadmeSample.Entities;
4+
5+
public class Product
26
{
3-
public class Product
4-
{
5-
public int Id { get; set; }
7+
public int Id { get; set; }
8+
public required string Name { get; set; }
9+
public decimal ListPrice { get; set; }
610

7-
public string Name { get; set; }
11+
// Optional supplier — foreign key is nullable so the join is a LEFT JOIN in SQL.
12+
public int? SupplierId { get; set; }
13+
public Supplier? Supplier { get; set; }
814

9-
public decimal ListPrice { get; set; }
10-
}
15+
/// <summary>
16+
/// Null-conditional rewriting (NullConditionalRewriteSupport.Ignore):
17+
/// the ?. operator is stripped and EF Core handles nullability via the LEFT JOIN.
18+
/// </summary>
19+
[Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Ignore)]
20+
public string? SupplierName => Supplier?.Name;
1121
}

0 commit comments

Comments
 (0)