Skip to content

Support file-scoped types as entity types#38215

Open
m-x-shokhzod wants to merge 2 commits intodotnet:mainfrom
m-x-shokhzod:fix/file-scoped-entity-types
Open

Support file-scoped types as entity types#38215
m-x-shokhzod wants to merge 2 commits intodotnet:mainfrom
m-x-shokhzod:fix/file-scoped-entity-types

Conversation

@m-x-shokhzod
Copy link
Copy Markdown
Contributor

Fixes #32323.

Problem

file class / file record declarations crash EF Core with IndexOutOfRangeException when used as entity types:

file class MyEntity { public int Id { get; set; } }

class MyContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder mb)
        => mb.Entity<MyEntity>().HasNoKey();
}

await context.Set<MyEntity>().ToListAsync();
// → IndexOutOfRangeException at NavigationExpandingExpressionVisitor.CreateNavigationExpansionExpression

The same problem reproduces via DbContext.Database.SqlQuery<MyEntity> (per @TwentyFourMinutes' comment on the issue).

Root cause

The C# compiler synthesizes the metadata name for a file-scoped type as <FileName>F<hex>__UserTypeName. IReadOnlyTypeBase.ShortName uses IndexOf("<") to strip generic type arguments from the short display name. For file-scoped types the leading < of the synthesized prefix is at index 0, so name[..0] produces an empty string and downstream consumers — for example NavigationExpandingExpressionVisitor.CreateNavigationExpansionExpression at entityType.ShortName()[0].ToString().ToLowerInvariant() — crash on the indexer.

Fix

Detect the synthesized file-scoped prefix specifically and skip past __ to expose the user-visible type name. The detection uses the <...>F signature, which distinguishes file-scoped types from other compiler-generated types whose names also begin with <:

Synthesized name Signature character after > Touched by this PR?
<>f__AnonymousType0 (anonymous) n/a (<> prefix, handled by existing branch) no
<File>F<hex>__Type (file-scoped) F yes — extracts Type
<Method>d__0 (async state machine) d no
<Method>g__Local|0_0 (local function) g no

The __ separator is searched starting after the closing >, so file names containing __ are not misparsed. Once located, only the first __ after the prefix is consumed, so user type names containing __ are preserved.

If any check in the new branch fails (e.g. no >, signature is not F, no __ follows), name is left unchanged and falls through to the existing generic-stripping logic. The fix is a strict refinement, not a replacement.

Tests

Added under EntityTypeTest.ShortName_…:

Positive — file-scoped synthesized names produce user-visible short names:

  • <Program>F1234ABCD__MyEntityMyEntity
  • <My__File>F1234ABCD__MyEntityMyEntity (filename contains __)
  • <Program>F1234ABCD__Foo__BarFoo__Bar (user name contains __)
  • <Program>F1234ABCD__MyEntity<int>MyEntity (file-scoped + generic)

Negative — regular types are not touched by the new branch:

  • FooFoo
  • Foo__BarFoo__Bar (__ alone is not a trigger)
  • MyType<int>MyType (existing generic strip)
  • Foo__Bar<int>Foo__Bar (combination)

The three pre-existing ShortName_on_compiler_generated_typeN tests covering <>-prefixed anonymous types continue to pass unchanged.

Verification

  • dotnet build src/EFCore/EFCore.csproj — clean, 0 warnings
  • dotnet test test/EFCore.Tests/EFCore.Tests.csproj6,804 / 6,804 pass (was 6,800; +4 new theory rows)
  • dotnet test test/EFCore.InMemory.FunctionalTests/...27,592 / 27,592 pass (full query pipeline confirmed unaffected)

What's intentionally not in this PR

The symptom site at NavigationExpandingExpressionVisitor.CreateNavigationExpansionExpression and the similar pattern at line 857 (methodCallExpression.Method.Name[0].ToString().ToLowerInvariant()) could also crash for synthesized method names, but those are out of scope here. This PR fixes the source — every consumer of ShortName() benefits without further changes. Happy to follow up on the method-name path in a separate PR if desired.

For `file class` / `file record` declarations, the C# compiler synthesizes
a metadata name of the form `<FileName>F<hex>__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<hex>`. 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 dotnet#32323
@m-x-shokhzod m-x-shokhzod requested a review from a team as a code owner May 2, 2026 13:58
Copy link
Copy Markdown
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks like a good fix - but can you fix DisplayName as well as ShortName (and add test coverage for that too)?

Extend the file-scoped type prefix stripping to DisplayName(), which
previously surfaced Roslyn's raw <FileName>F<hex>__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<hex>__
sentinel, including occurrences nested inside generic argument lists.
For example, List<<File>F1234__Inner> now becomes List<Inner> 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.
@m-x-shokhzod m-x-shokhzod requested a review from roji May 3, 2026 12:26
Copy link
Copy Markdown
Member

@roji roji left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, @AndriySvyryd leaving to you in case you want to take a final look - otherwise I'll merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support "file" scoped types as entity types

3 participants