dotnet / dotnet/wpf

MarkupCompilePass1 uses implementation assemblies instead of ref assemblies, breaking incremental build

Open
#11,579 4 comments 2 reactions 0 assignees View on GitHub
Bug Investigate
Dominant language
C#
Stars
7.7k
Forks
1.3k
Avg merge
1d 11h
Merged PRs (30d)
61

Description

## Summary

`MarkupCompilePass1` receives `References="@(ReferencePath)"` which resolves to **implementation DLL paths** for project references. When a referenced project recompiles — even for implementation-only changes that don't alter the public API — the implementation DLL timestamp changes and `MarkupCompilePass1` detects a "reference change," triggering a full recompilation of **all XAML files** in the project.

This defeats incremental build for any WPF project that references another project with `ProduceReferenceAssembly=true`.

## Expected behavior

When a referenced project recompiles but its **public API surface is unchanged** (ref assembly is identical), `MarkupCompilePass1` should detect no reference change and skip XAML recompilation.

## Actual behavior

`MarkupCompilePass1` sees the implementation DLL's new timestamp, concludes references changed, and recompiles every XAML file. This triggers:

1. New `.g.cs` files → new `.baml` files → `MainResourcesGeneration` runs → `CoreCompile` runs
2. For projects with local type references, `GenerateTemporaryTargetAssembly` creates a `*_wpftmp.csproj` → a **second** `CoreCompile` invocation

Each affected WPF project adds 1–2 extra `csc` invocations per incremental build.

## Reproduction

Given this project graph:

```
Lib.csproj (netstandard2.0, ProduceReferenceAssembly=true)
└─ referenced by WpfApp.csproj (net472, has XAML files)
```

1. Build the full graph (baseline)
2. Make an **implementation-only** change to `Lib.csproj` (e.g., change a string literal inside a method body)
3. Rebuild

**Expected**: 1 `csc` invocation (just `Lib.csproj`)
**Actual**: 3 `csc` invocations (`Lib.csproj` + `WpfApp.csproj` + `WpfApp_*_wpftmp.csproj`)

## Impact

In large solutions, a single impl-only change to a widely-referenced library cascades to every downstream WPF project. In our case (Visual Studio's Copilot solution), a one-line method body change triggered **6 csc invocations** instead of **1**, adding ~7 seconds of unnecessary compilation.

## Root cause

In [`Microsoft.WinFX.targets`](https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/Microsoft.WinFX.targets), the `MarkupCompilePass1` target passes:

```xml


<_OrigRefPathForMarkup Include="@(ReferencePath)" />
<_RefPathWithRefAsm Include="@(ReferencePath)"
Condition="'%(ReferencePath.ReferenceAssembly)' != '' and Exists('%(ReferencePath.ReferenceAssembly)')" />


<_RefPathWithRefAsm Remove="@(_RefPathWithRefAsm)" />




<_OrigRefPathForMarkup Remove="@(_OrigRefPathForMarkup)" />

```

## Environment

- .NET SDK 10.0.106
- PresentationBuildTasks 10.0.6.0
- `ProduceReferenceAssembly=true` (default for modern SDK projects)
- MSBuild 17.x (msbuild.exe, not `dotnet build`)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.