From e2c3898d4f6b39e944ed53613cdd2e5542c8d026 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 17:28:56 +0000
Subject: [PATCH 1/4] Initial plan
From 23a7b8f3aea3b4decd23c26d1e8a940a3e90e6d1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 17:34:21 +0000
Subject: [PATCH 2/4] Add GeneratorBenchmarks: source generator self-benchmark
with MemoryDiagnoser
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
...ameworkCore.Projectables.Benchmarks.csproj | 5 +-
.../GeneratorBenchmarks.cs | 129 ++++++++++++++++++
2 files changed, 133 insertions(+), 1 deletion(-)
create mode 100644 benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/EntityFrameworkCore.Projectables.Benchmarks.csproj b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/EntityFrameworkCore.Projectables.Benchmarks.csproj
index 345c4c2..592ea04 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/EntityFrameworkCore.Projectables.Benchmarks.csproj
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/EntityFrameworkCore.Projectables.Benchmarks.csproj
@@ -7,11 +7,14 @@
+
+
+
-
+
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
new file mode 100644
index 0000000..b5b1ddf
--- /dev/null
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
@@ -0,0 +1,129 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using EntityFrameworkCore.Projectables;
+using EntityFrameworkCore.Projectables.Generator;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+
+namespace EntityFrameworkCore.Projectables.Benchmarks;
+
+[MemoryDiagnoser]
+public class GeneratorBenchmarks
+{
+ [Params(1, 10, 50, 100)]
+ public int ProjectableCount { get; set; }
+
+ private Compilation _compilation = null!;
+ private GeneratorDriver _warmedDriver = null!;
+ private Compilation _modifiedCompilation = null!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _compilation = CreateCompilation(ProjectableCount);
+
+ // Warm up the driver once so incremental state is established.
+ _warmedDriver = CSharpGeneratorDriver
+ .Create(new ProjectionExpressionGenerator())
+ .RunGeneratorsAndUpdateCompilation(_compilation, out _, out _);
+
+ // Create a slightly modified compilation that doesn't affect any
+ // [Projectable] members — used to exercise the incremental cache path.
+ var originalTree = _compilation.SyntaxTrees.First();
+ var originalText = originalTree.GetText().ToString();
+ var modifiedText = originalText + "\n// bench-edit";
+ var modifiedTree = originalTree.WithChangedText(SourceText.From(modifiedText));
+ _modifiedCompilation = _compilation.ReplaceSyntaxTree(originalTree, modifiedTree);
+ }
+
+ /// Cold run: a fresh driver is created every iteration.
+ [Benchmark]
+ public GeneratorDriver RunGenerator()
+ {
+ return CSharpGeneratorDriver
+ .Create(new ProjectionExpressionGenerator())
+ .RunGeneratorsAndUpdateCompilation(_compilation, out _, out _);
+ }
+
+ ///
+ /// Warm incremental run: the pre-warmed driver processes a trivial one-line
+ /// edit that does not touch any [Projectable] member, exercising the
+ /// incremental caching path.
+ ///
+ [Benchmark]
+ public GeneratorDriver RunGenerator_Incremental()
+ {
+ return _warmedDriver
+ .RunGeneratorsAndUpdateCompilation(_modifiedCompilation, out _, out _);
+ }
+
+ private static Compilation CreateCompilation(int projectableCount)
+ {
+ var source = BuildSource(projectableCount);
+
+ var references = Basic.Reference.Assemblies.
+#if NET10_0
+ Net100
+#else
+ Net80
+#endif
+ .References.All.ToList();
+
+ references.Add(MetadataReference.CreateFromFile(typeof(ProjectableAttribute).Assembly.Location));
+
+ return CSharpCompilation.Create(
+ "GeneratorBenchmarkInput",
+ new[] { CSharpSyntaxTree.ParseText(source) },
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+
+ private static string BuildSource(int projectableCount)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using EntityFrameworkCore.Projectables;");
+ sb.AppendLine();
+ sb.AppendLine("namespace GeneratorBenchmarkInput;");
+ sb.AppendLine();
+ sb.AppendLine("public class Order");
+ sb.AppendLine("{");
+ sb.AppendLine(" public string FirstName { get; set; } = string.Empty;");
+ sb.AppendLine(" public string LastName { get; set; } = string.Empty;");
+ sb.AppendLine(" public string? Email { get; set; }");
+ sb.AppendLine(" public decimal Amount { get; set; }");
+ sb.AppendLine(" public decimal TaxRate { get; set; }");
+ sb.AppendLine(" public DateTime? DeletedAt { get; set; }");
+ sb.AppendLine(" public bool IsEnabled { get; set; }");
+ sb.AppendLine();
+
+ for (int i = 0; i < projectableCount; i++)
+ {
+ sb.AppendLine(" [Projectable]");
+ switch (i % 4)
+ {
+ case 0:
+ // Computed string property
+ sb.AppendLine($" public string FullName{i} => FirstName + \" \" + LastName;");
+ break;
+ case 1:
+ // Boolean flag property
+ sb.AppendLine($" public bool IsActive{i} => DeletedAt == null && IsEnabled;");
+ break;
+ case 2:
+ // Decimal method with single param
+ sb.AppendLine($" public decimal TotalWithTax{i}(decimal taxRate) => Amount * (1 + taxRate);");
+ break;
+ case 3:
+ // Multi-param method returning string
+ sb.AppendLine($" public string FormatSummary{i}(string prefix, int count) => prefix + \": \" + FirstName + \" x\" + count;");
+ break;
+ }
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+}
From 92908a9fc764fd1406c5c13a08df93508f9ba647 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 19:41:17 +0000
Subject: [PATCH 3/4] Expand GeneratorBenchmarks: cover all expression
transformers + projectable constructors
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../GeneratorBenchmarks.cs | 116 +++++++++++++++++-
1 file changed, 110 insertions(+), 6 deletions(-)
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
index b5b1ddf..1bb294a 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
@@ -87,6 +87,11 @@ private static string BuildSource(int projectableCount)
sb.AppendLine();
sb.AppendLine("namespace GeneratorBenchmarkInput;");
sb.AppendLine();
+
+ // Enum used by the switch-expression and is-pattern members.
+ sb.AppendLine("public enum OrderStatus { Pending, Active, Completed, Cancelled }");
+ sb.AppendLine();
+
sb.AppendLine("public class Order");
sb.AppendLine("{");
sb.AppendLine(" public string FirstName { get; set; } = string.Empty;");
@@ -96,34 +101,133 @@ private static string BuildSource(int projectableCount)
sb.AppendLine(" public decimal TaxRate { get; set; }");
sb.AppendLine(" public DateTime? DeletedAt { get; set; }");
sb.AppendLine(" public bool IsEnabled { get; set; }");
+ sb.AppendLine(" public int Priority { get; set; }");
+ sb.AppendLine(" public OrderStatus Status { get; set; }");
sb.AppendLine();
+ // Nine member kinds — one per transformer path in the generator:
+ // 0 simple string-concat property (ExpressionSyntaxRewriter baseline)
+ // 1 boolean null-check property (ExpressionSyntaxRewriter, logical AND)
+ // 2 single-param decimal method (ExpressionSyntaxRewriter, arithmetic)
+ // 3 multi-param string method (ExpressionSyntaxRewriter, concat)
+ // 4 null-conditional property (NullConditionalRewrite, Rewrite mode)
+ // 5 switch-expression method (SwitchExpressionRewrite, relational patterns)
+ // 6 is-pattern property (VisitIsPatternExpression, not-null pattern)
+ // 7 block-bodied if/else chain (BlockStatementConverter)
+ // 8 block-bodied switch with local var (BlockStatementConverter + local replacement)
for (int i = 0; i < projectableCount; i++)
{
- sb.AppendLine(" [Projectable]");
- switch (i % 4)
+ switch (i % 9)
{
case 0:
- // Computed string property
+ // Expression-bodied: simple string concatenation.
+ sb.AppendLine(" [Projectable]");
sb.AppendLine($" public string FullName{i} => FirstName + \" \" + LastName;");
break;
+
case 1:
- // Boolean flag property
+ // Expression-bodied: null check combined with logical AND.
+ sb.AppendLine(" [Projectable]");
sb.AppendLine($" public bool IsActive{i} => DeletedAt == null && IsEnabled;");
break;
+
case 2:
- // Decimal method with single param
+ // Expression-bodied: single-param decimal arithmetic.
+ sb.AppendLine(" [Projectable]");
sb.AppendLine($" public decimal TotalWithTax{i}(decimal taxRate) => Amount * (1 + taxRate);");
break;
+
case 3:
- // Multi-param method returning string
+ // Expression-bodied: multi-param string method.
+ sb.AppendLine(" [Projectable]");
sb.AppendLine($" public string FormatSummary{i}(string prefix, int count) => prefix + \": \" + FirstName + \" x\" + count;");
break;
+
+ case 4:
+ // Null-conditional member access — exercises NullConditionalRewrite transformer.
+ sb.AppendLine(" [Projectable(NullConditionalRewriteSupport = NullConditionalRewriteSupport.Rewrite)]");
+ sb.AppendLine($" public int? EmailLength{i} => Email?.Length;");
+ break;
+
+ case 5:
+ // Switch expression with relational patterns — exercises SwitchExpressionRewrite transformer.
+ sb.AppendLine(" [Projectable]");
+ sb.AppendLine($" public string GetGrade{i}(int score) => score switch {{ >= 90 => \"A\", >= 70 => \"B\", _ => \"C\" }};");
+ break;
+
+ case 6:
+ // Is-pattern (not-null) — exercises ExpressionSyntaxRewriter.VisitIsPatternExpression.
+ sb.AppendLine(" [Projectable]");
+ sb.AppendLine($" public bool HasEmail{i} => Email is not null;");
+ break;
+
+ case 7:
+ // Block-bodied if/else chain — exercises BlockStatementConverter.
+ sb.AppendLine(" [Projectable(AllowBlockBody = true)]");
+ sb.AppendLine($" public string GetStatusLabel{i}()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (DeletedAt != null)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return \"Deleted\";");
+ sb.AppendLine(" }");
+ sb.AppendLine(" if (IsEnabled)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return \"Active: \" + FirstName;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" return \"Inactive\";");
+ sb.AppendLine(" }");
+ break;
+
+ case 8:
+ // Block-bodied switch statement with a local variable — exercises
+ // BlockStatementConverter together with local-variable replacement.
+ sb.AppendLine(" [Projectable(AllowBlockBody = true)]");
+ sb.AppendLine($" public string GetPriorityName{i}()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var p = Priority;");
+ sb.AppendLine(" switch (p)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" case 1: return \"Low\";");
+ sb.AppendLine(" case 2: return \"Medium\";");
+ sb.AppendLine(" case 3: return \"High\";");
+ sb.AppendLine(" default: return \"Unknown\";");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ break;
}
+
sb.AppendLine();
}
sb.AppendLine("}");
+ sb.AppendLine();
+
+ // DTO classes with [Projectable] constructors — exercises the constructor
+ // projection path (ProjectableInterpreter constructor handling).
+ // Count scales proportionally: roughly one DTO per nine Order members.
+ int ctorCount = Math.Max(1, projectableCount / 9);
+ for (int j = 0; j < ctorCount; j++)
+ {
+ sb.AppendLine($"public class OrderSummaryDto{j}");
+ sb.AppendLine("{");
+ sb.AppendLine(" public string FullName { get; set; } = string.Empty;");
+ sb.AppendLine(" public decimal Total { get; set; }");
+ sb.AppendLine(" public bool IsActive { get; set; }");
+ sb.AppendLine();
+ // Parameterless constructor required by [Projectable] constructor support.
+ sb.AppendLine($" public OrderSummaryDto{j}() {{ }}");
+ sb.AppendLine();
+ sb.AppendLine(" [Projectable]");
+ sb.AppendLine($" public OrderSummaryDto{j}(string firstName, string lastName, decimal amount, decimal taxRate, bool isActive)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" FullName = firstName + \" \" + lastName;");
+ sb.AppendLine(" Total = amount * (1 + taxRate);");
+ sb.AppendLine(" IsActive = isActive && amount > 0;");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ }
+
return sb.ToString();
}
}
From 6b666524e64eb6fc947a94bf45318ca7af80698e Mon Sep 17 00:00:00 2001
From: "fabien.menager"
Date: Wed, 4 Mar 2026 21:36:46 +0100
Subject: [PATCH 4/4] Change counts
---
.../GeneratorBenchmarks.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
index 1bb294a..be761cf 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/GeneratorBenchmarks.cs
@@ -11,7 +11,7 @@ namespace EntityFrameworkCore.Projectables.Benchmarks;
[MemoryDiagnoser]
public class GeneratorBenchmarks
{
- [Params(1, 10, 50, 100)]
+ [Params(1, 100, 1000)]
public int ProjectableCount { get; set; }
private Compilation _compilation = null!;