Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions src/EFCore/Metadata/IReadOnlyTypeBase.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -86,7 +87,7 @@ string DisplayName(bool omitSharedType)
{
if (!HasSharedClrType)
{
return ClrType.ShortDisplayName();
return StripFileScopedTypePrefixes(ClrType.ShortDisplayName());
}

var shortName = Name;
Expand Down Expand Up @@ -131,16 +132,20 @@ 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
{
return name;
// File-scoped types: Roslyn synthesizes the metadata name
// <FileName>F<hex>__UserTypeName for `file class` / `file record` declarations.
// Strip these sentinels wherever they appear (top-level or nested in generic args),
// so e.g. List<<File>F1234__Inner> becomes List<Inner>.
name = StripFileScopedTypePrefixes(name);
}

return name[..lessIndex];
var lessIndex = name.IndexOf('<', StringComparison.Ordinal);
return lessIndex == -1 ? name : name[..lessIndex];
}

var hashIndex = Name.LastIndexOf("#", StringComparison.Ordinal);
Expand All @@ -161,6 +166,63 @@ string ShortName()
return Name[(hashIndex + 1)..];
}

/// <summary>
/// Strips Roslyn's synthesized file-scoped type prefix (<c>&lt;FileName&gt;F&lt;hex&gt;__</c>)
/// from a CLR display name, including occurrences nested inside generic argument lists.
/// For example <c>List&lt;&lt;Program&gt;F1234__Inner&gt;</c> becomes <c>List&lt;Inner&gt;</c>.
/// </summary>
/// <remarks>
/// The <c>&gt;F</c> signature distinguishes file-scoped types from other compiler-generated
/// types whose names begin with <c>&lt;</c> (async state machines <c>&lt;Method&gt;d__0</c>,
/// local function host classes <c>&lt;Method&gt;g__Local|0_0</c>, anonymous types
/// <c>&lt;&gt;f__AnonymousType</c>, closure display classes <c>&lt;&gt;c__DisplayClass</c>).
/// Filenames cannot contain <c>&lt;</c> or <c>&gt;</c>, so the closing <c>&gt;</c> of a
/// sentinel is always the next <c>&gt;</c> after the opening <c>&lt;</c> with no
/// intervening <c>&lt;</c>.
/// </remarks>
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<hex>__` 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;
}

/// <summary>
/// Determines whether the current type can be assigned to the specified type, i.e. is derived from or identical to it.
/// </summary>
Expand Down
143 changes: 143 additions & 0 deletions test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3032,6 +3032,149 @@ public void ShortName_on_compiler_generated_type3()
Assert.Equal("__AnonymousType01Child", entityType.ShortName());
}

[ConditionalTheory]
// file class MyEntity in Program.cs
[InlineData("<Program>F1234ABCD__MyEntity", "MyEntity")]
// file class declared in a file whose name itself contains "__"
[InlineData("<My__File>F1234ABCD__MyEntity", "MyEntity")]
// file class whose user-chosen name contains "__"
[InlineData("<Program>F1234ABCD__Foo__Bar", "Foo__Bar")]
// file class with generic type parameters
[InlineData("<Program>F1234ABCD__MyEntity<int>", "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<int>", "MyType")]
// Generic type whose name contains "__"
[InlineData("Foo__Bar<int>", "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());
}

[ConditionalTheory]
// file class MyEntity in Program.cs
[InlineData("<Program>F1234ABCD__MyEntity", "MyEntity")]
// file class declared in a file whose name itself contains "__"
[InlineData("<My__File>F1234ABCD__MyEntity", "MyEntity")]
// file class whose user-chosen name contains "__"
[InlineData("<Program>F1234ABCD__Foo__Bar", "Foo__Bar")]
// file class with single generic type parameter — DisplayName preserves generics (unlike ShortName)
[InlineData("<Program>F1234ABCD__MyEntity<int>", "MyEntity<int>")]
// file class with nested generics
[InlineData("<Program>F1234ABCD__Wrapper<List<int>>", "Wrapper<List<int>>")]
// file class used as a generic argument inside another type — sentinel must be stripped from the inner position too
[InlineData("List<<Program>F1234ABCD__MyFileClass>", "List<MyFileClass>")]
// generic of generic, with a file-scoped type at the inner-inner position
[InlineData("List<List<<Program>F1234ABCD__Inner>>", "List<List<Inner>>")]
// short hex digest (Roslyn varies digest length)
[InlineData("<Program>F12__Foo", "Foo")]
// long hex digest
[InlineData("<Program>FABCDEF1234567890__Foo", "Foo")]
// file class whose user-chosen name starts with an underscore
[InlineData("<Program>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<int>", "MyType<int>")]
// Generic type whose name contains "__"
[InlineData("Foo__Bar<int>", "Foo__Bar<int>")]
// 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("<MyMethod>d__0", "<MyMethod>d__0")]
// Local function — `>g` signature, not `>F`, untouched
[InlineData("<MyMethod>g__Local|0_0", "<MyMethod>g__Local|0_0")]
// Lowercase `f` — Roslyn anonymous-type marker, not file-scoped (`F` is uppercase). Untouched.
[InlineData("<Program>f1234__NotFileScoped", "<Program>f1234__NotFileScoped")]
// Has `>F` signature but no `__` separator — malformed, leave alone
[InlineData("<Program>F1234ABCD", "<Program>F1234ABCD")]
// `<` but no closing `>` — malformed, leave alone
[InlineData("<NoClose", "<NoClose")]
// Empty user portion after `__` — malformed Roslyn output, but ensure we don't crash;
// current behavior strips to empty (matches ShortName behavior; harmless edge case)
[InlineData("<Program>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<Customer>", CreateModel().AddEntityType(typeof(List<Customer>)).DisplayName());
}

private readonly IMutableModel _model = BuildModel();

private IMutableEntityType DependentType
Expand Down
Loading