Skip to content

PresentationBuildTasks emits BamlRecordType.Property (5) instead of PropertyWithConverter (36) for Nullable<T> properties #11815

Description

@AndreyMarten

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:

  1. builds Repro.Legacy (non-SDK, TargetFrameworkVersion v4.7.2) with MSBuild.exe → in-box PresentationBuildTasks 4.0.0.0 from the GAC;
  2. builds Repro.Sdk (SDK-style, net8.0-windows) with dotnet buildPresentationBuildTasks from the .NET SDK;
  3. 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.

Metadata

Metadata

Assignees

Labels

BugProduct bug (most likely)Cost:SWork that requires one engineer up to 1 weekPR ProposedPriority:2Work that is important, but not critical for the releasearea-infrastructuretenet-compatibilityIncompatibility with previous versions or with WPF for .NET Framework

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions