Skip to content

[mono][interp] GetType() on a Nullable<T> returns Nullable`1 on WebAssembly in .NET 9/10 (regression of #61007) #132461

Closed

Description

@Kebechet

Description

On Blazor WebAssembly, calling GetType() on a Nullable<T> returns System.Nullable1[[T]]instead of the underlying typeT`.

This is a regression of #61007, which was fixed in November 2021 by #61020 (src/mono/mono/mini/interp/transform.c). It behaves correctly on .NET 8 and is broken again on .NET 9 and .NET 10.

The failing shape is the sample published in the Nullable value types reference under How to identify a nullable value type, which documents the contract being violated:

Because boxing a non-null instance of a nullable value type is equivalent to boxing a value of the underlying type, GetType returns a Type instance that represents the underlying type of a nullable value type.

The failure is silent, which is what makes it costly in practice. A widespread helper shape resolves an enum to a CSS class or a localization key through [Description]:

var enumField = value.GetType().GetField(value.ToString());
return enumField is null
    ? value.ToString().ToLower()   // silently taken on .NET 9 / .NET 10 WASM
    : enumField.GetCustomAttribute<DescriptionAttribute>()!.Description;

GetField misses, the fallback returns the lowercased member name, and every styled element renders with a class that does not exist. No exception and no log entry. In our case this silently removed all typography and design-token colours across a large Blazor WebAssembly app after a .NET 8 to .NET 10 upgrade.

Reproduction Steps

Repro repository: https://github.com/Kebechet/nullable-gettype-wasm-repro

It contains three Blazor WebAssembly apps that differ only in TargetFramework and the matching Microsoft.AspNetCore.Components.WebAssembly version, and share a byte-identical Root.razor.

git clone https://github.com/Kebechet/nullable-gettype-wasm-repro
cd nullable-gettype-wasm-repro
dotnet run --project src/Net10     # broken
dotnet run --project src/Net9      # broken
dotnet run --project src/Net8      # correct

Open the page and read the <pre id="result"> block.

Smallest possible form:

int? a = 17;
Console.WriteLine(a.GetType().FullName);

Expected behavior

System.Int32

Actual behavior

System.Nullable`1[[System.Int32, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]

Full matrix from the repro:

check .NET 8.0.25 .NET 9.0.14 .NET 10.0.5
int? a = 17; a.GetType() System.Int32 System.Nullable1[[System.Int32]]` System.Nullable1[[System.Int32]]`
M<T>(T? v) => v.GetType() Style System.Nullable1[[Style]]` System.Nullable1[[Style]]`
[Description] via value.GetType().GetField(name) p24-bd miss, falls back to extralargebold miss, falls back to extralargebold
typeof(T).GetField(name) p24-bd p24-bd p24-bd
value.Value.GetType() Style Style Style
box stored in an object local, then .GetType() System.Int32 System.Int32 System.Int32

Not enum-specific: int?, Guid?, DateTime? and decimal? all reproduce identically.

Regression?

Yes.

  • Correct on .NET 8 (8.0.25)
  • Broken on .NET 9 (9.0.14) and .NET 10 (10.0.5), identically

It is the runtime rather than codegen. The IL of the failing method is byte-identical between the Net8 and Net9 builds, verified by hashing the method body via System.Reflection.Metadata with the same SDK (10.0.201) compiling both, yet the two runtimes disagree.

For the same reason the C# 14 field contextual keyword is not involved: .NET 9 defaults to C# 13, which predates the feature, and the failing generic method declares no identifier named field. The locals in the repro are named enumField to remove that question.

Known Workarounds

Any of these return the correct type on all three runtimes:

  • typeof(T) instead of value.GetType() inside a generic
  • value.Value.GetType()
  • assign the box to an object local first, then call GetType() on that local

Configuration

  • .NET SDK 10.0.201 and 9.0.312, Windows 11 x64
  • Blazor WebAssembly, Debug, interpreter (no AOT), Chrome
  • RuntimeInformation.FrameworkDescription values as reported by each app are in the table above

Other information

Roslyn emits a plain box !T? followed by callvirt instance Type Object::GetType() in both the failing and the working shapes. Dumping the method bodies shows exactly one box (0x8C) and zero constrained. prefixes (0xFE 0x16) in each, so the constrained. path is not what differs.

What distinguishes them is whether the box is immediately consumed by GetType(). That suggests the interpreter is again statically folding box <nullable> plus GetType() into "return the constrained class type", exactly as described in #61020:

We were statically optimizing this call to return the actual constrained class type, which is incorrect for nullables, because boxing of a nullable (as part of the constrained call) actually creates an object with the type of the nullable's value (or null if there is no value).

Storing the box into an object local breaks the pattern up and the correct type comes back, which is consistent with that.

#61020 added regression tests at src/tests/JIT/Directed/nullabletypes/gettype.cs. Those live under src/tests/JIT/, so it may be worth checking whether they run in the wasm interpreter configuration, since this went unnoticed across two releases.

Activity

  1. dotnet-policy-service commented on Aug 18, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @steveisok, @dotnet/area-system-reflection
    See info in area-owners.md if you want to be subscribed.

  2. Kebechet commented on Aug 18, 2026

    @Kebechet
    Author

    Also reproduces on .NET 11 preview 7, so this is still live in main rather than something already fixed and pending release.

    RuntimeInformation.FrameworkDescription: .NET 11.0.0-preview.7.26381.103, built with SDK 11.0.100-preview.7.26381.103 and Microsoft.AspNetCore.Components.WebAssembly 11.0.0-preview.7.26381.103.

    1. int? a = 17; a.GetType().FullName
       expected : System.Int32
       actual   : System.Nullable`1[[System.Int32, System.Private.CoreLib, Version=11.0.0.0, ...]]
    
    2. M<T>(T? v) => v.GetType()
       expected : Repro.Root+Style
       actual   : System.Nullable`1[[Repro.Root+Style, Net11, ...]]
    
    3. [Description] resolved via value.GetType().GetField(name)
       expected : p24-bd
       actual   : (GetField missed, silently fell back to 'extralargebold')
    

    The three workarounds still behave correctly on preview 7 (typeof(T).GetField(name), value.Value.GetType(), and storing the box in an object local before calling GetType()).

    Updated matrix, all measured from the repro:

    check 8.0.25 9.0.14 10.0.5 11.0.0-preview.7
    int? a = 17; a.GetType() System.Int32 Nullable1` Nullable1` Nullable1`
    M<T>(T? v) => v.GetType() Style Nullable1` Nullable1` Nullable1`
    [Description] via value.GetType() p24-bd miss miss miss
    typeof(T).GetField(name) p24-bd p24-bd p24-bd p24-bd
    value.Value.GetType() Style Style Style Style
    box in an object local, then .GetType() System.Int32 System.Int32 System.Int32 System.Int32

    The repro repository now contains a fourth project, src/Net11, sharing the same Root.razor as the others: https://github.com/Kebechet/nullable-gettype-wasm-repro

  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Sep 1, 2026
  4. added this to the 11.0.0 milestone on Sep 1, 2026
  5. added a commit that references this issue on Sep 3, 2026
    bbe4da3
  6. locked and limited conversation to collaborators on Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions