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() {