Repository navigation
Description
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,
GetTypereturns aTypeinstance 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 # correctOpen 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 ofvalue.GetType()inside a genericvalue.Value.GetType()- assign the box to an
objectlocal first, then callGetType()on that local
Configuration
- .NET SDK 10.0.201 and 9.0.312, Windows 11 x64
- Blazor WebAssembly, Debug, interpreter (no AOT), Chrome
RuntimeInformation.FrameworkDescriptionvalues 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
dotnet-policy-service commentedon Aug 18, 2026
Tagging subscribers to this area: @steveisok, @dotnet/area-system-reflection
See info in area-owners.md if you want to be subscribed.
Kebechet commentedon Aug 18, 2026
Also reproduces on .NET 11 preview 7, so this is still live in
mainrather than something already fixed and pending release.RuntimeInformation.FrameworkDescription:.NET 11.0.0-preview.7.26381.103, built with SDK11.0.100-preview.7.26381.103andMicrosoft.AspNetCore.Components.WebAssembly11.0.0-preview.7.26381.103.The three workarounds still behave correctly on preview 7 (
typeof(T).GetField(name),value.Value.GetType(), and storing the box in anobjectlocal before callingGetType()).Updated matrix, all measured from the repro:
int? a = 17; a.GetType()System.Int32Nullable1`Nullable1`Nullable1`M<T>(T? v) => v.GetType()StyleNullable1`Nullable1`Nullable1`[Description]viavalue.GetType()p24-bdtypeof(T).GetField(name)p24-bdp24-bdp24-bdp24-bdvalue.Value.GetType()StyleStyleStyleStyleobjectlocal, then.GetType()System.Int32System.Int32System.Int32System.Int32The repro repository now contains a fourth project,
src/Net11, sharing the sameRoot.razoras the others: https://github.com/Kebechet/nullable-gettype-wasm-repro[mono][interp] Preserve nullable boxing for GetType (#132732)