Description
Two PresentationBuildTasks compile the same XAML differently. For a Nullable<T> property, the in-box .NET Framework one (4.8, from the GAC) writes BamlRecordType.PropertyWithConverter (36), while the one shipped in the .NET SDK writes BamlRecordType.Property (5). Non-nullable properties are identical (record 36) in both.
Record 36 carries a trailing converterTypeId; record 5 does not. WPF's loader reads both, so it is invisible at runtime, but it breaks tools that read or rewrite compiled BAML.
The record type is decided by project style (SDK-style vs non-SDK), not by TargetFramework and not by the build runner: an SDK-style net472 project yields record 5 via both dotnet build and MSBuild.exe.
Reproduction Steps
The attached solution (dxSample.zip) compiles one two-line XAML twice and prints the record type each compiler emitted.
NullableProbe.xaml:
<SolidColorBrush x:Key="plain" Color="#FF112233" /> <!-- Color (struct) -> control -->
<ColorAnimation x:Key="anim" To="#FF445566" /> <!-- Color? (Nullable<T>) -> bug -->
Run build.cmd, which:
- builds
Repro.Legacy (non-SDK, TargetFrameworkVersion v4.7.2) with MSBuild.exe → in-box PresentationBuildTasks 4.0.0.0 from the GAC;
- builds
Repro.Sdk (SDK-style, net8.0-windows) with dotnet build → PresentationBuildTasks from the .NET SDK;
Check reads the compiled .baml out of each assembly's *.g.resources and reports the record type byte for each value.
Expected behavior
Both compilers emit PropertyWithConverter (36) for the Nullable<T> property, as the in-box compiler does:
| property |
type |
.NET Framework |
.NET SDK |
SolidColorBrush.Color |
Color |
36 |
36 |
ColorAnimation.To |
Color? |
36 |
36 |
Actual behavior
The SDK compiler writes plain Property (5) for the nullable property:
| property |
type |
.NET Framework |
.NET SDK |
SolidColorBrush.Color |
Color |
36 |
36 |
ColorAnimation.To |
Color? |
36 |
5 |
This affects every Nullable<T> property, not just colors (DoubleAnimation.To, double?, behaves the same).
Check also demonstrates the root cause directly, resolving ColorAnimation.To in a MetadataLoadContext:
GetGenericTypeDefinition() == typeof(Nullable<>) : False (should be True)
IsNullableType(...) : False
runtime typeof(Nullable<int>) IsNullableType : True (sanity check)
Regression?
No. The behavior differs between the .NET Framework and the .NET (SDK) WPF compilers.
Known Workarounds
None. Keeping projects non-SDK-style avoids it, but is not viable long-term.
Impact
This affects every Nullable<T> property and breaks any tool that reads or rewrites compiled BAML — localizers, theme/palette generators, and other post-build BAML processors — because the record layout no longer matches what the .NET Framework compiler produced.
In DevExpress WPF it silently corrupts runtime theming, producing wrong colors on .NET while the identical markup is correct on .NET Framework. The divergence is also a migration hazard: the .NET Framework build is correct only because it is still a non-SDK project, so moving those projects to SDK-style would regress it the same way.
Configuration
- .NET SDK 10.0.302 (reproduces with the SDK's
tools\net472 task, i.e. independent of TargetFramework, and independent of dotnet build vs MSBuild.exe — what matters is SDK-style vs non-SDK project)
- In-box
PresentationBuildTasks 4.8.9032.0
- Windows 11
Other information
Possible cause
ReflectionHelper.IsNullableType (Shared/System/Windows/Markup/ReflectionHelper.cs) compares by Type identity:
internal static bool IsNullableType(Type type) =>
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
Under the SDK compiler type comes from a reflection-only context, so GetGenericTypeDefinition() returns that context's Nullable<>, not the runtime typeof(Nullable<>), and == is false (see part 2 of Check). The PBTCOMPILER branch in GetKnownConverterTypeFromType (BamlMapTable.cs) then never returns NullableConverter, and WriteProperty (BamlRecordWriter.cs) falls through to the plain record 5. The 4.8 compiler runs on .NET Framework against the same WPF assemblies it loaded (runtime types), so there the comparison is true and record 36 is written.
Description
Two
PresentationBuildTaskscompile the same XAML differently. For aNullable<T>property, the in-box .NET Framework one (4.8, from the GAC) writesBamlRecordType.PropertyWithConverter(36), while the one shipped in the .NET SDK writesBamlRecordType.Property(5). Non-nullable properties are identical (record 36) in both.Record 36 carries a trailing
converterTypeId; record 5 does not. WPF's loader reads both, so it is invisible at runtime, but it breaks tools that read or rewrite compiled BAML.The record type is decided by project style (SDK-style vs non-SDK), not by
TargetFrameworkand not by the build runner: an SDK-stylenet472project yields record 5 via bothdotnet buildandMSBuild.exe.Reproduction Steps
The attached solution (dxSample.zip) compiles one two-line XAML twice and prints the record type each compiler emitted.
NullableProbe.xaml:Run
build.cmd, which:Repro.Legacy(non-SDK,TargetFrameworkVersion v4.7.2) withMSBuild.exe→ in-boxPresentationBuildTasks 4.0.0.0from the GAC;Repro.Sdk(SDK-style,net8.0-windows) withdotnet build→PresentationBuildTasksfrom the .NET SDK;Checkreads the compiled.bamlout of each assembly's*.g.resourcesand reports the record type byte for each value.Expected behavior
Both compilers emit
PropertyWithConverter(36) for theNullable<T>property, as the in-box compiler does:SolidColorBrush.ColorColorColorAnimation.ToColor?Actual behavior
The SDK compiler writes plain
Property(5) for the nullable property:SolidColorBrush.ColorColorColorAnimation.ToColor?This affects every
Nullable<T>property, not just colors (DoubleAnimation.To,double?, behaves the same).Checkalso demonstrates the root cause directly, resolvingColorAnimation.Toin aMetadataLoadContext:Regression?
No. The behavior differs between the .NET Framework and the .NET (SDK) WPF compilers.
Known Workarounds
None. Keeping projects non-SDK-style avoids it, but is not viable long-term.
Impact
This affects every
Nullable<T>property and breaks any tool that reads or rewrites compiled BAML — localizers, theme/palette generators, and other post-build BAML processors — because the record layout no longer matches what the .NET Framework compiler produced.In DevExpress WPF it silently corrupts runtime theming, producing wrong colors on .NET while the identical markup is correct on .NET Framework. The divergence is also a migration hazard: the .NET Framework build is correct only because it is still a non-SDK project, so moving those projects to SDK-style would regress it the same way.
Configuration
tools\net472task, i.e. independent ofTargetFramework, and independent ofdotnet buildvsMSBuild.exe— what matters is SDK-style vs non-SDK project)PresentationBuildTasks4.8.9032.0Other information
Possible cause
ReflectionHelper.IsNullableType(Shared/System/Windows/Markup/ReflectionHelper.cs) compares byTypeidentity:Under the SDK compiler
typecomes from a reflection-only context, soGetGenericTypeDefinition()returns that context'sNullable<>, not the runtimetypeof(Nullable<>), and==isfalse(see part 2 ofCheck). ThePBTCOMPILERbranch inGetKnownConverterTypeFromType(BamlMapTable.cs) then never returnsNullableConverter, andWriteProperty(BamlRecordWriter.cs) falls through to the plain record 5. The 4.8 compiler runs on .NET Framework against the same WPF assemblies it loaded (runtime types), so there the comparison istrueand record 36 is written.