PresentationBuildTasks emits `BamlRecordType.Property` (5) instead of `PropertyWithConverter` (36) for `Nullable<T>` properties
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
### Description
Two `PresentationBuildTasks` compile the same XAML differently. For a `Nullable` 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](https://github.com/user-attachments/files/30696634/dxSample.zip)**) compiles one two-line XAML twice and prints the record type each compiler emitted.
`NullableProbe.xaml`:
```xml
```
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 build` → `PresentationBuildTasks` 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` 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` 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) 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` 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:
```csharp
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.
Contributor guide
Assessment
This issue has not been assessed yet.