diff --git a/plugins/dotnet-msbuild/agents/build-perf.agent.md b/plugins/dotnet-msbuild/agents/build-perf.agent.md index ae0ea50e8a..3eb0d53268 100644 --- a/plugins/dotnet-msbuild/agents/build-perf.agent.md +++ b/plugins/dotnet-msbuild/agents/build-perf.agent.md @@ -76,6 +76,7 @@ Load these skills for detailed guidance on specific optimization areas: - `build-parallelism` — Parallelism and graph build - `eval-performance` — Evaluation performance - `check-bin-obj-clash` — Output path conflicts +- `copy-to-output-directory` — Removing the `Always` copy perf hit (`IfDifferent`, `$(SkipUnchangedFilesOnCopyAlways)`) ## Important Notes - Always use `/bl` to generate binlogs for data-driven analysis diff --git a/plugins/dotnet-msbuild/agents/msbuild-code-review.agent.md b/plugins/dotnet-msbuild/agents/msbuild-code-review.agent.md index ab2bb90161..73ef314f66 100644 --- a/plugins/dotnet-msbuild/agents/msbuild-code-review.agent.md +++ b/plugins/dotnet-msbuild/agents/msbuild-code-review.agent.md @@ -76,3 +76,4 @@ This agent draws knowledge from these companion skills — load them for detaile - `directory-build-organization` — Build infrastructure organization - `check-bin-obj-clash` — Output path conflict detection - `incremental-build` — Incremental build correctness +- `copy-to-output-directory` — CopyToOutputDirectory mode selection; replacing the `Always` perf anti-pattern with `IfDifferent` diff --git a/plugins/dotnet-msbuild/agents/msbuild.agent.md b/plugins/dotnet-msbuild/agents/msbuild.agent.md index 5557edff47..249b2f19b8 100644 --- a/plugins/dotnet-msbuild/agents/msbuild.agent.md +++ b/plugins/dotnet-msbuild/agents/msbuild.agent.md @@ -39,6 +39,7 @@ Classify the user's request and route to the appropriate specialist: | Modernize legacy projects | `msbuild-code-review` agent + `msbuild-modernization` skill | | Organize build infrastructure | This agent + `directory-build-organization` skill | | Incremental build broken | This agent + `incremental-build` skill | +| Choosing/fixing CopyToOutputDirectory behavior (`IfDifferent`, `Always` perf hit) | This agent + `copy-to-output-directory` skill | When routing to a specialized agent, provide context about the user's request so the agent can pick up seamlessly. @@ -82,6 +83,7 @@ This agent has access to a comprehensive set of troubleshooting and optimization - `directory-build-organization` — Directory.Build infrastructure - `check-bin-obj-clash` — Output path conflict detection - `including-generated-files` — Build-generated file inclusion +- `copy-to-output-directory` — CopyToOutputDirectory mode selection (`Never`/`PreserveNewest`/`Always`/`IfDifferent`) ## Common Troubleshooting Patterns diff --git a/plugins/dotnet-msbuild/skills/copy-to-output-directory/SKILL.md b/plugins/dotnet-msbuild/skills/copy-to-output-directory/SKILL.md new file mode 100644 index 0000000000..d6737e4d71 --- /dev/null +++ b/plugins/dotnet-msbuild/skills/copy-to-output-directory/SKILL.md @@ -0,0 +1,100 @@ +--- +name: copy-to-output-directory +description: "Choosing an MSBuild CopyToOutputDirectory / CopyToPublishDirectory mode: Never, PreserveNewest, Always, and IfDifferent (MSBuild 17.13+), plus $(SkipUnchangedFilesOnCopyAlways). USE FOR: removing the per-build Always copy perf hit; resetting output files mutated between builds. DO NOT USE FOR: general incremental-build diagnosis (use incremental-build); non-MSBuild build systems." +license: MIT +--- + +# Choosing a CopyToOutputDirectory Mode + +## Overview + +The `CopyToOutputDirectory` metadata (and its publish counterpart `CopyToPublishDirectory`) controls whether an item — `Content`, `None`, `EmbeddedResource`, or `Compile` — is copied next to your build output, and under what conditions the copy happens. Picking the wrong mode causes either stale files in `bin/` or an unnecessary per-build performance hit. + +As of **MSBuild 17.13 / .NET SDK 9.0.2xx** there are four values: + +| Mode | Copies when… | Incremental cost | Typical use | +| --- | --- | --- | --- | +| `Never` (default) | Never | None | Files not needed at runtime | +| `PreserveNewest` | Source is **newer** than destination (or destination missing) | Cheap (timestamp check) | The common case — source files you edit | +| `Always` | **Every build**, unconditionally | Expensive — copies on every build even in no-op builds | Legacy workaround; avoid (see below) | +| `IfDifferent` | Source **differs** from destination in either direction (newer **or** older, or size differs, or destination missing) | Cheap (timestamp + size check) | Destination may be mutated between builds | + +```xml + + + + +``` + +You can use either the attribute form shown above or the child-element form: + +```xml + + IfDifferent + +``` + +## Why `Always` is usually the wrong choice + +`Always` re-copies the file on **every** build, including otherwise-clean incremental/no-op builds. On projects with many or large content files this is a measurable, recurring cost and a common cause of "why is my no-op build not instant?" reports. + +Historically `Always` was the only way to handle a specific scenario: **the destination file can change between builds** — for example an SQLite database, a storage/state file, or a config file that a test run mutates. With `PreserveNewest`, if the destination is modified (making its timestamp *newer* than the source) MSBuild will *not* restore the original source file, because the source is no longer newer. People reached for `Always` to force the file back into a known-good state — paying the copy cost on every build as a side effect. + +## `IfDifferent`: copy when different, in either direction + +`IfDifferent` is the targeted fix for that scenario. It copies the source over the destination whenever MSBuild considers the two **different** — whether the source is newer *or* older than the destination, whether the size differs, or the destination is missing — and skips the copy when the destination is unchanged per MSBuild's heuristic. + +Under the hood the `_CopyDifferingSourceItemsToOutputDirectory` target uses the `Copy` task with `SkipUnchangedFiles="true"`. That "unchanged" check is a **heuristic**: it compares **last-write timestamp and file size only** — not a content hash — so a destination that was edited to the same size and timestamp as the source is treated as unchanged and is *not* re-copied. In practice this restores a mutated destination back to the source version on the next build (the reason people reached for `Always`) while avoiding the unconditional per-build copy. + +Use `IfDifferent` when: + +- A test run or the app itself writes to the copied file (databases, caches, state/storage files, editable config) and you want each build to reset it to the source version. +- You were using `Always` purely as a "keep the output in sync with the source" mechanism, not because you truly need a copy on every single build. + +```xml + + + + +``` + +## Globally softening `Always` with `$(SkipUnchangedFilesOnCopyAlways)` + +If you have an existing codebase full of `CopyToOutputDirectory="Always"` items and want the performance benefit without editing every item, set the property: + +```xml + + true + +``` + +This makes the `_CopyOutOfDateSourceItemsToOutputDirectoryAlways` target pass `SkipUnchangedFiles="true"` to its `Copy` task, so `Always` items are only copied when they actually differ — effectively giving `Always` the same skip-unchanged behavior as `IfDifferent`. + +- Default is `false` for backwards compatibility (classic `Always` = copy every build). +- Set it in `Directory.Build.props` to opt an entire repo in at once. +- Prefer converting individual items to `IfDifferent` when you can; use this property when a bulk, non-invasive opt-in is more practical. + +## How the modes flow through the build + +`GetCopyToOutputDirectoryItems` buckets each item by its `CopyToOutputDirectory` value. Three copy targets then do the work as dependencies of `_CopySourceItemsToOutputDirectory` (which is itself invoked by `CopyFilesToOutputDirectory`): + +- `_CopyOutOfDateSourceItemsToOutputDirectory` — `PreserveNewest` items (incremental via `Inputs`/`Outputs` timestamp comparison). +- `_CopyOutOfDateSourceItemsToOutputDirectoryAlways` — `Always` items (unconditional copy unless `$(SkipUnchangedFilesOnCopyAlways)` is `true`). +- `_CopyDifferingSourceItemsToOutputDirectory` — `IfDifferent` items (`SkipUnchangedFiles="true"`). + +All copied files are registered in `FileWrites`, so `dotnet clean` removes them. + +**Transitive copy:** items marked `Always`, `PreserveNewest`, or `IfDifferent` also flow to referencing projects through `ProjectReference` (via `_CopyToOutputDirectoryTransitiveItems`). `Never` items do not. `IfDifferent` participates in ClickOnce publish item collection alongside `Always`/`PreserveNewest`. + +## Version requirement + +`IfDifferent` and `$(SkipUnchangedFilesOnCopyAlways)` require **MSBuild 17.13 or later** (**.NET SDK 9.0.2xx+ / Visual Studio 2022 17.13+**). On older toolsets the value is not recognized: it will not match the `Always`/`PreserveNewest`/`IfDifferent` conditions in the common targets, so the item is silently **not copied**. Gate usage on the toolset if you must support older SDKs, or require the minimum SDK via `global.json`. + +## Quick decision guide + +- Don't need the file at runtime → `Never` (or omit — it's the default). +- Normal source file you edit → `PreserveNewest`. +- Destination gets mutated between builds and must be reset to the source → `IfDifferent`. +- You truly need a fresh copy on literally every build → `Always` (rare). +- Stuck with lots of legacy `Always` and want the perf win without edits → keep `Always` but set `$(SkipUnchangedFilesOnCopyAlways)=true`. diff --git a/plugins/dotnet-msbuild/skills/property-patterns/SKILL.md b/plugins/dotnet-msbuild/skills/property-patterns/SKILL.md index 6c8f5c19a0..c0e3793c7b 100644 --- a/plugins/dotnet-msbuild/skills/property-patterns/SKILL.md +++ b/plugins/dotnet-msbuild/skills/property-patterns/SKILL.md @@ -1,6 +1,6 @@ --- name: property-patterns -description: "MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing slash handling, TFM detection helpers, and property evaluation order. USE FOR: diagnosing and fixing MSBuild property definition issues in .props or .csproj files, reviewing and fixing shared property configuration anti-patterns, fixing DefineConstants or NoWarn being overwritten instead of appended, fixing unconditional property assignments that prevent project-level overrides, fixing unquoted conditions that fail when properties are empty, fixing hardcoded paths that break cross-platform builds, setting property defaults that can be overridden, understanding property evaluation order and last-write-wins semantics. DO NOT USE FOR: props vs targets placement (use directory-build-organization), item operations (use item-management), target structure (use target-authoring), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +description: "MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing-slash handling, TFM detection helpers, and evaluation order. USE FOR: diagnosing and fixing property definition issues and shared-property anti-patterns in .props/.csproj; DefineConstants or NoWarn overwritten instead of appended; unconditional assignments that block project-level overrides; unquoted conditions that fail on empty properties; hardcoded paths that break cross-platform builds; setting overridable defaults; property evaluation order and last-write-wins semantics. DO NOT USE FOR: props vs targets placement (use directory-build-organization), item operations (use item-management), target structure (use target-authoring), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." license: MIT --- diff --git a/plugins/dotnet-msbuild/skills/target-authoring/SKILL.md b/plugins/dotnet-msbuild/skills/target-authoring/SKILL.md index 0e72084bc1..c54248036d 100644 --- a/plugins/dotnet-msbuild/skills/target-authoring/SKILL.md +++ b/plugins/dotnet-msbuild/skills/target-authoring/SKILL.md @@ -1,6 +1,6 @@ --- name: target-authoring -description: "Canonical patterns for writing custom MSBuild targets. USE FOR: diagnosing and fixing custom target authoring anti-patterns, reviewing MSBuild target definitions for correctness, diagnosing broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets), fixing targets that replace CompileDependsOn instead of extending it with $(CompileDependsOn), fixing query targets that return stale results due to Outputs vs Returns misuse, fixing missing Inputs/Outputs causing unnecessary rebuilds, fixing missing FileWrites registration. Covers DependsOnTargets vs BeforeTargets vs AfterTargets, the Build→CoreBuild three-level pattern, hooking into the build pipeline, the $(XxxDependsOn) chain-extension pattern. DO NOT USE FOR: incremental build tuning (use incremental-build), parallelization (use build-parallelism), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." +description: "Canonical patterns for writing custom MSBuild targets. USE FOR: diagnosing and fixing custom target authoring anti-patterns; broken SDK target chains across files (e.g., Directory.Build.targets silently redefining SDK targets); targets that replace CompileDependsOn instead of extending it with $(CompileDependsOn); query targets returning stale results from Outputs vs Returns misuse; missing Inputs/Outputs causing unnecessary rebuilds; missing FileWrites registration. Covers DependsOnTargets vs BeforeTargets vs AfterTargets, the Build→CoreBuild three-level pattern, and the $(XxxDependsOn) chain-extension pattern. DO NOT USE FOR: incremental build tuning (use incremental-build), parallelization (use build-parallelism), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." license: MIT ---