diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionClassNameGenerator.cs index bfaab69..84ac27b 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 0000000..39e2716 --- /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 b9bcaa7..4aaa91b 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 cf0df91..248ec57 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() {