dotnet / dotnet/msbuild

GetTargetPathWithTargetPlatformMoniker defeats reference-assembly incrementality for project references in design-time FUTD

Open
#14,023 1 comment 0 reactions 1 assignee Claimed by @ViktorHofer View on GitHub
triaged
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.