From e38f42dcbe58f3d16d5f2e3fa53d1a579c715059 Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Sat, 2 May 2026 18:57:54 +0500 Subject: [PATCH 1/2] Support `file` scoped types as entity types For `file class` / `file record` declarations, the C# compiler synthesizes a metadata name of the form `F__UserTypeName`. The existing implementation of `IReadOnlyTypeBase.ShortName` looks for the first `<` in the type's short display name to strip generic type arguments. For file-scoped types that `<` is at index 0, so the truncation produced an empty string. Consumers such as `NavigationExpandingExpressionVisitor.CreateNavigationExpansionExpression` then crashed with `IndexOutOfRangeException` when indexing the result. Detect the synthesized prefix via the `<...>F` signature (`F` distinguishes file-scoped types from async state machines `<...>d__N` and local function host classes `<...>g__Local|N_N`) and skip past the `__` separator that appears after `>F`. The search for `__` is bounded to start after the closing `>`, so file names containing `__` are not misparsed; user type names containing `__` are preserved because the search uses the first occurrence within the suffix only. Fixes #32323 --- src/EFCore/Metadata/IReadOnlyTypeBase.cs | 26 ++++++++-- .../Metadata/Internal/EntityTypeTest.cs | 52 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/EFCore/Metadata/IReadOnlyTypeBase.cs b/src/EFCore/Metadata/IReadOnlyTypeBase.cs index 36def55e87e..d4e80bcaff7 100644 --- a/src/EFCore/Metadata/IReadOnlyTypeBase.cs +++ b/src/EFCore/Metadata/IReadOnlyTypeBase.cs @@ -131,16 +131,32 @@ string ShortName() var name = ClrType.ShortDisplayName(); if (name.StartsWith("<>", StringComparison.Ordinal)) { + // Anonymous and closure types: <>f__AnonymousType0, <>c__DisplayClass0_0, ... name = name[2..]; } - - var lessIndex = name.IndexOf("<", StringComparison.Ordinal); - if (lessIndex == -1) + else if (name.Length > 2 && name[0] == '<') { - return name; + // File-scoped types: Roslyn synthesizes the metadata name + // F__UserTypeName for `file class` / `file record` declarations. + // The `>F` signature distinguishes file-scoped types from other compiler-generated + // types whose names also begin with `<` (async state machines `d__0`, + // local function host classes `g__Local|0_0`, etc.). For those the + // signature character following `>` is `d`, `g`, etc., never `F`. + var closeAngle = name.IndexOf('>', 1); + if (closeAngle != -1 + && closeAngle + 1 < name.Length + && name[closeAngle + 1] == 'F') + { + var separator = name.IndexOf("__", closeAngle + 1, StringComparison.Ordinal); + if (separator != -1) + { + name = name[(separator + 2)..]; + } + } } - return name[..lessIndex]; + var lessIndex = name.IndexOf('<', StringComparison.Ordinal); + return lessIndex == -1 ? name : name[..lessIndex]; } var hashIndex = Name.LastIndexOf("#", StringComparison.Ordinal); diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index d2669fa1be2..94a00cf297f 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -3032,6 +3032,58 @@ public void ShortName_on_compiler_generated_type3() Assert.Equal("__AnonymousType01Child", entityType.ShortName()); } + [ConditionalTheory] + // file class MyEntity in Program.cs + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + // file class declared in a file whose name itself contains "__" + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + // file class whose user-chosen name contains "__" + [InlineData("F1234ABCD__Foo__Bar", "Foo__Bar")] + // file class with generic type parameters + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + public void ShortName_on_file_scoped_type(string clrName, string expectedShortName) + { + var model = CreateModel(); + + var assemblyName = new AssemblyName("DynamicEntityClrTypeAssembly_FileScoped"); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule"); + var typeBuilder = moduleBuilder.DefineType(clrName); + var type = typeBuilder.CreateType(); + + model.AddEntityType(type); + + var entityType = model.FinalizeModel().FindEntityType(clrName); + + Assert.Equal(expectedShortName, entityType.ShortName()); + } + + [ConditionalTheory] + // Regular type — no `<` prefix, no transformation + [InlineData("Foo", "Foo")] + // Type whose user-chosen name contains "__" but is not file-scoped — no `<` prefix, no transformation + [InlineData("Foo__Bar", "Foo__Bar")] + // Generic type — existing logic still strips generics from the tail + [InlineData("MyType", "MyType")] + // Generic type whose name contains "__" + [InlineData("Foo__Bar", "Foo__Bar")] + public void ShortName_unchanged_for_regular_types(string clrName, string expectedShortName) + { + var model = CreateModel(); + + var assemblyName = new AssemblyName("DynamicEntityClrTypeAssembly_Regular"); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule"); + var typeBuilder = moduleBuilder.DefineType(clrName); + var type = typeBuilder.CreateType(); + + model.AddEntityType(type); + + var entityType = model.FinalizeModel().FindEntityType(clrName); + + Assert.Equal(expectedShortName, entityType.ShortName()); + } + private readonly IMutableModel _model = BuildModel(); private IMutableEntityType DependentType From 498ba93086d8005a400bff056700866bf890120f Mon Sep 17 00:00:00 2001 From: m_axwel_l Date: Sun, 3 May 2026 14:42:53 +0500 Subject: [PATCH 2/2] Strip file-scoped type prefix in DisplayName and nested generic args Extend the file-scoped type prefix stripping to DisplayName(), which previously surfaced Roslyn's raw F__UserName synthesized name to users in error messages. Both ShortName() and DisplayName() now share a private static helper that scans the entire CLR display name and elides every <...>F__ sentinel, including occurrences nested inside generic argument lists. For example, List<F1234__Inner> now becomes List instead of being left ugly. Add positive theory rows covering nested generics, varying hex digest lengths, leading-underscore user names, and negative rows pinning that async state machines (>d), local functions (>g), closures (<>c), anonymous types (<>f), and lowercase-f Roslyn markers remain untouched. --- src/EFCore/Metadata/IReadOnlyTypeBase.cs | 80 ++++++++++++---- .../Metadata/Internal/EntityTypeTest.cs | 91 +++++++++++++++++++ 2 files changed, 154 insertions(+), 17 deletions(-) diff --git a/src/EFCore/Metadata/IReadOnlyTypeBase.cs b/src/EFCore/Metadata/IReadOnlyTypeBase.cs index d4e80bcaff7..dfbe0a3c377 100644 --- a/src/EFCore/Metadata/IReadOnlyTypeBase.cs +++ b/src/EFCore/Metadata/IReadOnlyTypeBase.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Metadata; @@ -86,7 +87,7 @@ string DisplayName(bool omitSharedType) { if (!HasSharedClrType) { - return ClrType.ShortDisplayName(); + return StripFileScopedTypePrefixes(ClrType.ShortDisplayName()); } var shortName = Name; @@ -134,25 +135,13 @@ string ShortName() // Anonymous and closure types: <>f__AnonymousType0, <>c__DisplayClass0_0, ... name = name[2..]; } - else if (name.Length > 2 && name[0] == '<') + else { // File-scoped types: Roslyn synthesizes the metadata name // F__UserTypeName for `file class` / `file record` declarations. - // The `>F` signature distinguishes file-scoped types from other compiler-generated - // types whose names also begin with `<` (async state machines `d__0`, - // local function host classes `g__Local|0_0`, etc.). For those the - // signature character following `>` is `d`, `g`, etc., never `F`. - var closeAngle = name.IndexOf('>', 1); - if (closeAngle != -1 - && closeAngle + 1 < name.Length - && name[closeAngle + 1] == 'F') - { - var separator = name.IndexOf("__", closeAngle + 1, StringComparison.Ordinal); - if (separator != -1) - { - name = name[(separator + 2)..]; - } - } + // Strip these sentinels wherever they appear (top-level or nested in generic args), + // so e.g. List<F1234__Inner> becomes List. + name = StripFileScopedTypePrefixes(name); } var lessIndex = name.IndexOf('<', StringComparison.Ordinal); @@ -177,6 +166,63 @@ string ShortName() return Name[(hashIndex + 1)..]; } + /// + /// Strips Roslyn's synthesized file-scoped type prefix (<FileName>F<hex>__) + /// from a CLR display name, including occurrences nested inside generic argument lists. + /// For example List<<Program>F1234__Inner> becomes List<Inner>. + /// + /// + /// The >F signature distinguishes file-scoped types from other compiler-generated + /// types whose names begin with < (async state machines <Method>d__0, + /// local function host classes <Method>g__Local|0_0, anonymous types + /// <>f__AnonymousType, closure display classes <>c__DisplayClass). + /// Filenames cannot contain < or >, so the closing > of a + /// sentinel is always the next > after the opening < with no + /// intervening <. + /// + private static string StripFileScopedTypePrefixes(string name) + { + if (name.IndexOf('<', StringComparison.Ordinal) == -1) + { + return name; + } + + StringBuilder? sb = null; + var i = 0; + while (i < name.Length) + { + if (name[i] == '<') + { + // Look for the immediately-following `>F__` sentinel: + // - the next `>` must come without any nested `<` in between (filenames have neither) + // - the char right after `>` must be `F` + // - a `__` must follow (the prefix terminator) + var closeAngle = name.IndexOf('>', i + 1); + if (closeAngle != -1 + && closeAngle + 1 < name.Length + && name[closeAngle + 1] == 'F') + { + var nestedLt = name.IndexOf('<', i + 1, closeAngle - i - 1); + if (nestedLt == -1) + { + var separator = name.IndexOf("__", closeAngle + 1, StringComparison.Ordinal); + if (separator != -1) + { + sb ??= new StringBuilder(name.Length).Append(name, 0, i); + i = separator + 2; + continue; + } + } + } + } + + sb?.Append(name[i]); + i++; + } + + return sb?.ToString() ?? name; + } + /// /// Determines whether the current type can be assigned to the specified type, i.e. is derived from or identical to it. /// diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 94a00cf297f..6a297e4cbcc 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -3084,6 +3084,97 @@ public void ShortName_unchanged_for_regular_types(string clrName, string expecte Assert.Equal(expectedShortName, entityType.ShortName()); } + [ConditionalTheory] + // file class MyEntity in Program.cs + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + // file class declared in a file whose name itself contains "__" + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + // file class whose user-chosen name contains "__" + [InlineData("F1234ABCD__Foo__Bar", "Foo__Bar")] + // file class with single generic type parameter — DisplayName preserves generics (unlike ShortName) + [InlineData("F1234ABCD__MyEntity", "MyEntity")] + // file class with nested generics + [InlineData("F1234ABCD__Wrapper>", "Wrapper>")] + // file class used as a generic argument inside another type — sentinel must be stripped from the inner position too + [InlineData("List<F1234ABCD__MyFileClass>", "List")] + // generic of generic, with a file-scoped type at the inner-inner position + [InlineData("ListF1234ABCD__Inner>>", "List>")] + // short hex digest (Roslyn varies digest length) + [InlineData("F12__Foo", "Foo")] + // long hex digest + [InlineData("FABCDEF1234567890__Foo", "Foo")] + // file class whose user-chosen name starts with an underscore + [InlineData("F1234ABCD___Underscored", "_Underscored")] + public void DisplayName_on_file_scoped_type(string clrName, string expectedDisplayName) + { + var model = CreateModel(); + + var assemblyName = new AssemblyName("DynamicEntityClrTypeAssembly_FileScopedDisplay"); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule"); + var typeBuilder = moduleBuilder.DefineType(clrName); + var type = typeBuilder.CreateType(); + + model.AddEntityType(type); + + var entityType = model.FinalizeModel().FindEntityType(clrName); + + Assert.Equal(expectedDisplayName, entityType.DisplayName()); + } + + [ConditionalTheory] + // Regular type — no `<` prefix, no transformation + [InlineData("Foo", "Foo")] + // Type whose user-chosen name contains "__" but is not file-scoped + [InlineData("Foo__Bar", "Foo__Bar")] + // Generic type — DisplayName preserves generics (unlike ShortName) + [InlineData("MyType", "MyType")] + // Generic type whose name contains "__" + [InlineData("Foo__Bar", "Foo__Bar")] + // Anonymous-style synthesized name (`<>`) — not file-scoped, untouched by file-scoped branch + [InlineData("<>__AnonymousType01Child", "<>__AnonymousType01Child")] + // Closure display class — `<>c` prefix, not `>F`, untouched + [InlineData("<>c__DisplayClass0_0", "<>c__DisplayClass0_0")] + // Async state machine — `>d` signature, not `>F`, untouched + [InlineData("d__0", "d__0")] + // Local function — `>g` signature, not `>F`, untouched + [InlineData("g__Local|0_0", "g__Local|0_0")] + // Lowercase `f` — Roslyn anonymous-type marker, not file-scoped (`F` is uppercase). Untouched. + [InlineData("f1234__NotFileScoped", "f1234__NotFileScoped")] + // Has `>F` signature but no `__` separator — malformed, leave alone + [InlineData("F1234ABCD", "F1234ABCD")] + // `<` but no closing `>` — malformed, leave alone + [InlineData("F1234ABCD__", "")] + // Length exactly 2 — guarded by `name.Length > 2`, falls through unchanged + [InlineData("<>", "<>")] + public void DisplayName_unchanged_for_non_file_scoped_types(string clrName, string expectedDisplayName) + { + var model = CreateModel(); + + var assemblyName = new AssemblyName("DynamicEntityClrTypeAssembly_DisplayRegular"); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule"); + var typeBuilder = moduleBuilder.DefineType(clrName); + var type = typeBuilder.CreateType(); + + model.AddEntityType(type); + + var entityType = model.FinalizeModel().FindEntityType(clrName); + + Assert.Equal(expectedDisplayName, entityType.DisplayName()); + } + + [ConditionalFact] + public void DisplayName_unchanged_for_well_known_types() + { + Assert.Equal("EntityTypeTest", CreateModel().AddEntityType(typeof(EntityTypeTest)).DisplayName()); + Assert.Equal("Customer", CreateModel().AddEntityType(typeof(Customer)).DisplayName()); + Assert.Equal("List", CreateModel().AddEntityType(typeof(List)).DisplayName()); + } + private readonly IMutableModel _model = BuildModel(); private IMutableEntityType DependentType