From 1813fbe2841b57e5d727a8260c80f953595aca00 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 16 May 2026 06:48:59 +0000
Subject: [PATCH 1/2] Initial plan
From 6479be2be62b444a6457c4edd8357d2f966b91c6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 16 May 2026 06:56:22 +0000
Subject: [PATCH 2/2] Fix source generator failing with @ verbatim identifier
prefix in parameter type names
Agent-Logs-Url: https://github.com/EFNext/EntityFrameworkCore.Projectables/sessions/c8748592-1460-4fb8-bd4d-b8eb20d15ce3
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../ProjectionExpressionClassNameGenerator.cs | 10 +++++++
...hVerbatimKeywordParameterType.verified.txt | 17 ++++++++++++
.../ExtensionMethodTests.cs | 26 +++++++++++++++++++
...ectionExpressionClassNameGeneratorTests.cs | 25 ++++++++++++++++++
4 files changed, 78 insertions(+)
create mode 100644 tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.ProjectableExtensionMethod_WithVerbatimKeywordParameterType.verified.txt
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
index bfaab699..84ac27b5 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs
@@ -140,6 +140,16 @@ private static void AppendSanitizedTypeName(StringBuilder sb, string typeName)
continue;
}
+ // Skip the verbatim identifier prefix '@' — it is a C# syntactic escape for
+ // reserved keywords (e.g. '@event') and has no meaning at the CLR level.
+ // The CLR type name is just 'event', so stripping '@' keeps the generated name
+ // consistent with the runtime resolver's output.
+ if (typeName[i] == '@')
+ {
+ i++;
+ continue;
+ }
+
var c = typeName[i];
sb.Append(IsInvalidIdentifierChar(c) ? '_' : c);
i++;
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.ProjectableExtensionMethod_WithVerbatimKeywordParameterType.verified.txt b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.ProjectableExtensionMethod_WithVerbatimKeywordParameterType.verified.txt
new file mode 100644
index 00000000..39e27161
--- /dev/null
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.ProjectableExtensionMethod_WithVerbatimKeywordParameterType.verified.txt
@@ -0,0 +1,17 @@
+//
+#nullable disable
+using System;
+using EntityFrameworkCore.Projectables;
+using Foo;
+
+namespace EntityFrameworkCore.Projectables.Generated
+{
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ static class Foo_EventExtensions_GetId_P0_Foo_event
+ {
+ static global::System.Linq.Expressions.Expression> Expression()
+ {
+ return (global::Foo.@event e) => e.Id;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.cs b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.cs
index b9bcaa75..4aaa91ba 100644
--- a/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Generator.Tests/ExtensionMethodTests.cs
@@ -105,6 +105,32 @@ static class C {
return Verifier.Verify(result.GeneratedTrees[0].ToString());
}
+ [Fact]
+ public Task ProjectableExtensionMethod_WithVerbatimKeywordParameterType()
+ {
+ var compilation = CreateCompilation(@"
+using System;
+using EntityFrameworkCore.Projectables;
+namespace Foo {
+ class @event {
+ public int Id { get; set; }
+ }
+
+ static class EventExtensions {
+ [Projectable]
+ public static int GetId(this @event e) => e.Id;
+ }
+}
+");
+
+ var result = RunGenerator(compilation);
+
+ Assert.Empty(result.Diagnostics);
+ Assert.Single(result.GeneratedTrees);
+
+ return Verifier.Verify(result.GeneratedTrees[0].ToString());
+ }
+
[Fact]
public Task ProjectableExtensionMethod_WithExpressionPropertyBody()
{
diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs
index cf0df91d..248ec57b 100644
--- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs
+++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectionExpressionClassNameGeneratorTests.cs
@@ -50,6 +50,31 @@ public void GenerateName_WithGlobalPrefixInGenericArgs_StripsAllGlobalPrefixes(
Assert.Equal(expected, result);
}
+ ///
+ /// Verifies that the verbatim identifier prefix @ is stripped from parameter type
+ /// names. Roslyn's FullyQualifiedFormat includes it for types whose names are
+ /// reserved C# keywords (e.g. @event), but the CLR runtime name never includes
+ /// @ — so both sides must agree on the sanitised name.
+ ///
+ [Theory]
+ [InlineData(
+ "global::Foo.Storage.@event",
+ "ns_a_m_P0_Foo_Storage_event")]
+ [InlineData(
+ "@event",
+ "ns_a_m_P0_event")]
+ [InlineData(
+ "global::Foo.@delegate",
+ "ns_a_m_P0_Foo_delegate")]
+ public void GenerateName_WithVerbatimAtPrefixInParamType_StripsAtSign(
+ string paramTypeName, string expected)
+ {
+ var result = ProjectionExpressionClassNameGenerator.GenerateName(
+ "ns", new[] { "a" }, "m", new[] { paramTypeName });
+
+ Assert.Equal(expected, result);
+ }
+
[Fact]
public void GeneratedFullName()
{