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!;