GetTargetPathWithTargetPlatformMoniker defeats reference-assembly incrementality for project references in design-time FUTD
- Dominant language
- C#
- Stars
- 5.5k
- Forks
- 1.5k
- Avg merge
- 1d 8h
- Merged PRs (30d)
- 141
Description
## Issue Description
`GetTargetPathWithTargetPlatformMoniker` in `Microsoft.Common.CurrentVersion.targets` appears to publish project-reference outputs in a way that defeats reference-assembly-based incrementality for Visual Studio design-time up-to-date checks.
In the scenario below, an implementation-only change in a referenced project invalidates a consuming project even though:
- the referenced project produces a reference assembly
- the consuming project is intended to compile against that reference assembly
- the consuming project does not copy the referenced implementation assembly locally (`Private=false` / `CopyLocal=false`)
The key problem I found is that for project references, `GetTargetPathWithTargetPlatformMoniker` carries forward implementation-oriented metadata:
- item identity is the implementation output path
- `CopyUpToDateMarker` is preserved
- `ReferenceAssembly` is present, but the emitted item still describes the implementation output as the authoritative artifact
In Visual Studio design-time builds, that flows into the data used by the fast up-to-date check, so the consuming project tracks the referenced implementation DLL and the referenced project's `.Up2Date` marker instead of only tracking the reference assembly.
That means implementation-only changes on the referenced project can incorrectly make the consuming project not up to date.
This seems directly related to the design intent in [#1986](https://github.com/dotnet/msbuild/issues/1986): when a reference assembly is available, implementation-only changes should not force downstream rebuilds.
## Broken Scenario
Concrete scenario from my investigation:
- Project A: SDK-style project that produces a reference assembly (for example a `netstandard2.0` library with `ProduceReferenceAssembly=true`)
- Project B: SDK-style Visual Studio project that references A via `ProjectReference`
- Project B does **not** copy A's implementation assembly locally (`Private=false` / `CopyLocal=false`)
- Project B is compiled/design-time-evaluated against A's reference assembly
- A source file in Project A changes in a way that does **not** change the reference assembly
Expected result:
- Project B should remain up to date, because its compilation contract has not changed and it is not copying A's implementation output locally
Actual result:
- Visual Studio's fast up-to-date check considers Project A's implementation DLL and/or A's `.Up2Date` marker as an input for Project B, so Project B is deemed not up to date
## What I Found
During investigation I traced the issue through the design-time targets and initially normalized `ReferencePathWithRefAssemblies`, which fixed part of the story but did **not** fully fix the up-to-date behavior.
The remaining decisive input came from `GetTargetPathWithTargetPlatformMoniker` in `Microsoft.Common.CurrentVersion.targets`:
```xml
$(TargetPlatformMoniker)
$(TargetPlatformIdentifier)
$(TargetFrameworkIdentifier)
$(TargetFrameworkVersion.TrimStart('vV'))
$(TargetRefPath)
@(CopyUpToDateMarker)
```
For project references this means the emitted item still describes the implementation output, while also carrying the referenced project's copy-up-to-date marker.
In my repro, the design-time up-to-date input still contained the referenced project's implementation DLL and `.Up2Date` marker even after compilation references had been normalized to the ref assembly path.
## Recommended Fix
I think the central fix belongs in `GetTargetPathWithTargetPlatformMoniker` (or a closely-related common target), not in downstream repo-specific workarounds.
For project references where `ReferenceAssembly` is available, MSBuild should stop surfacing the implementation output and copied marker as the authoritative design-time incremental input.
From what I found, the fix likely needs to do both of these for project references:
1. Use reference-assembly provenance for the design-time-facing item when `ReferenceAssembly` exists.
2. Clear `CopyUpToDateMarker` for non-copy-local project references (`Private=false`).
In my local workaround, the effective behavior was:
- normalize project-reference provenance back to the ref assembly
- clear `CopyUpToDateMarker` for non-copy-local references
- for `TargetPathWithTargetPlatformMoniker`, re-materialize the project-reference item using the ref assembly path rather than the implementation path
I would expect a proper MSBuild fix to provide equivalent semantics centrally so downstream toolsets do not need custom design-time normalization.
## Additional Information: Current Workaround
This is the verbatim workaround I am currently carrying downstream while waiting for a common-target fix:
```xml
%(ReferencePathWithRefAssemblies.ReferenceAssembly)
%(ReferencePathWithRefAssemblies.ReferenceAssembly)
<_ProjectReferenceTargetPathWithRefAsm
Include="@(TargetPathWithTargetPlatformMoniker)"
Condition="'%(TargetPathWithTargetPlatformMoniker.ReferenceSourceTarget)' == 'ProjectReference' and '%(TargetPathWithTargetPlatformMoniker.ReferenceAssembly)' != '' and Exists('%(TargetPathWithTargetPlatformMoniker.ReferenceAssembly)')" />
%(_ProjectReferenceTargetPathWithRefAsm.ReferenceAssembly)
%(_ProjectReferenceTargetPathWithRefAsm.ReferenceAssembly)
<_ProjectReferenceTargetPathWithRefAsm Remove="@(_ProjectReferenceTargetPathWithRefAsm)" />
```
## Environment
Observed in a Visual Studio design-time / fast up-to-date scenario, but the root cause appears to be the common MSBuild target behavior in `Microsoft.Common.CurrentVersion.targets`.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.