copy-to-output-directory

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Choosing a CopyToOutputDirectory Mode

选择CopyToOutputDirectory模式

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:
ModeCopies when…Incremental costTypical use
Never
(default)
NeverNoneFiles 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, unconditionallyExpensive — copies on every build even in no-op buildsLegacy 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
<ItemGroup>
  <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  <None Include="testdata\seed.db"  CopyToOutputDirectory="IfDifferent" />
</ItemGroup>
You can use either the attribute form shown above or the child-element form:
xml
<None Include="testdata\seed.db">
  <CopyToOutputDirectory>IfDifferent</CopyToOutputDirectory>
</None>
CopyToOutputDirectory
元数据(及其发布对应项
CopyToPublishDirectory
)用于控制项目项——
Content
None
EmbeddedResource
Compile
——是否复制到构建输出目录旁,以及复制触发的条件。选择错误的模式会导致
bin/
目录中存在过期文件,或带来不必要的每次构建性能损耗。
MSBuild 17.13 / .NET SDK 9.0.2xx版本开始,共有四种取值:
模式复制时机增量构建成本典型用途
Never
(默认)
从不复制运行时不需要的文件
PreserveNewest
源文件新于目标文件(或目标文件缺失)低成本(仅检查时间戳)常见场景——你编辑的源文件
Always
每次构建无条件复制高成本——即使在无操作构建中也会每次复制遗留解决方案;尽量避免(见下文)
IfDifferent
源文件与目标文件存在差异(无论源文件比目标文件新或旧,或大小不同,或目标文件缺失)低成本(检查时间戳+文件大小)目标文件可能在构建之间被修改的场景
xml
<ItemGroup>
  <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  <None Include="testdata\seed.db"  CopyToOutputDirectory="IfDifferent" />
</ItemGroup>
你可以使用上述属性形式,也可以使用子元素形式:
xml
<None Include="testdata\seed.db">
  <CopyToOutputDirectory>IfDifferent</CopyToOutputDirectory>
</None>

Why
Always
is usually the wrong choice

为什么
Always
通常不是正确选择

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.
Always
会在每次构建时重新复制文件,包括原本干净的增量/无操作构建。对于包含大量或大型内容文件的项目,这会带来可感知的持续性性能损耗,也是“为什么我的无操作构建不是瞬间完成?”这类问题的常见原因。
历史上,
Always
是处理特定场景的唯一方式:目标文件可能在构建之间被修改——例如SQLite数据库、存储/状态文件,或被测试运行修改的配置文件。使用
PreserveNewest
模式时,如果目标文件被修改(使其时间戳新于源文件),MSBuild将不会恢复原始源文件,因为源文件不再是更新的版本。人们选择
Always
来强制将文件恢复到已知良好状态——但副作用是每次构建都要付出复制成本。

IfDifferent
: copy when different, in either direction

IfDifferent
:存在差异时双向复制

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
<ItemGroup>
  <!-- Reset the fixture DB to the source copy whenever it has drifted,
       but don't pay a copy on every no-op build. -->
  <None Include="fixtures\catalog.db" CopyToOutputDirectory="IfDifferent" />
</ItemGroup>
IfDifferent
是针对该场景的针对性解决方案。每当MSBuild判定源文件和目标文件存在差异时——无论源文件比目标文件新或旧、大小不同,或目标文件缺失——它都会将源文件复制到目标文件;当MSBuild的启发式算法判定目标文件未更改时,则跳过复制。
在底层,
_CopyDifferingSourceItemsToOutputDirectory
目标使用带有
SkipUnchangedFiles="true"
参数的
Copy
任务。这里的“未更改”检查是一种启发式判断:仅比较最后写入时间戳和文件大小——而非内容哈希——因此如果目标文件被编辑后与源文件的大小和时间戳相同,会被视为未更改,不会重新复制。在实际使用中,这会在下一次构建时将被修改的目标文件恢复为源文件版本(这正是人们选择
Always
的原因),同时避免了无条件的每次构建复制操作。
在以下场景中使用
IfDifferent
  • 测试运行或应用本身会写入已复制的文件(数据库、缓存、状态/存储文件、可编辑配置),且你希望每次构建都将其重置为源文件版本。
  • 你使用
    Always
    纯粹是为了“保持输出与源文件同步”,而非真正需要在每次构建时都进行复制。
xml
<ItemGroup>
  <!-- 当测试用例数据库发生偏离时,将其重置为源文件副本,
       但不在每次无操作构建时付出复制成本。 -->
  <None Include="fixtures\catalog.db" CopyToOutputDirectory="IfDifferent" />
</ItemGroup>

Globally softening
Always
with
$(SkipUnchangedFilesOnCopyAlways)

使用
$(SkipUnchangedFilesOnCopyAlways)
全局弱化
Always
模式

If you have an existing codebase full of
CopyToOutputDirectory="Always"
items and want the performance benefit without editing every item, set the property:
xml
<PropertyGroup>
  <SkipUnchangedFilesOnCopyAlways>true</SkipUnchangedFilesOnCopyAlways>
</PropertyGroup>
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.
如果你的现有代码库中大量使用
CopyToOutputDirectory="Always"
的项目项,且希望无需编辑每个项就能获得性能提升,可以设置以下属性:
xml
<PropertyGroup>
  <SkipUnchangedFilesOnCopyAlways>true</SkipUnchangedFilesOnCopyAlways>
</PropertyGroup>
这会让
_CopyOutOfDateSourceItemsToOutputDirectoryAlways
目标向其
Copy
任务传递
SkipUnchangedFiles="true"
参数,因此仅当
Always
模式的项目项确实存在差异时才会复制——实际上让
Always
模式拥有与
IfDifferent
相同的跳过未更改文件行为。
  • 为保持向后兼容性,默认值为
    false
    (传统
    Always
    模式=每次构建都复制)。
  • Directory.Build.props
    中设置此属性,可一次性为整个代码库启用该优化。
  • 尽可能将单个项目项转换为
    IfDifferent
    模式;当需要批量、无侵入式启用优化时,再使用此属性。

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
.
GetCopyToOutputDirectoryItems
会根据项目项的
CopyToOutputDirectory
值进行分类。随后三个复制目标会作为
_CopySourceItemsToOutputDirectory
(由
CopyFilesToOutputDirectory
调用)的依赖项执行复制工作:
  • _CopyOutOfDateSourceItemsToOutputDirectory
    —— 处理
    PreserveNewest
    模式的项目项(通过
    Inputs
    /
    Outputs
    时间戳比较实现增量构建)。
  • _CopyOutOfDateSourceItemsToOutputDirectoryAlways
    —— 处理
    Always
    模式的项目项(无条件复制,除非
    $(SkipUnchangedFilesOnCopyAlways)
    设为
    true
    )。
  • _CopyDifferingSourceItemsToOutputDirectory
    —— 处理
    IfDifferent
    模式的项目项(
    SkipUnchangedFiles="true"
    )。
所有被复制的文件都会注册到
FileWrites
中,因此
dotnet clean
会将其删除。
传递复制:标记为
Always
PreserveNewest
IfDifferent
的项目项也会通过
ProjectReference
(经由
_CopyToOutputDirectoryTransitiveItems
)流转到引用项目。
Never
模式的项目项则不会。
IfDifferent
会与
Always
/
PreserveNewest
一起参与ClickOnce发布项集合。

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
.
IfDifferent
$(SkipUnchangedFilesOnCopyAlways)
需要MSBuild 17.13或更高版本.NET SDK 9.0.2xx+ / Visual Studio 2022 17.13+)。在旧版工具集中,该取值不会被识别:它无法匹配通用目标中的
Always
/
PreserveNewest
/
IfDifferent
条件,因此项目项会被静默不复制。如果必须支持旧版SDK,请根据工具集版本控制使用,或通过
global.json
要求最低SDK版本。

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
    .
  • 运行时不需要该文件 →
    Never
    (或省略——这是默认值)。
  • 正常编辑的源文件 →
    PreserveNewest
  • 目标文件在构建之间会被修改,且需要重置为源文件版本 →
    IfDifferent
  • 确实需要在每次构建时都获得全新副本 →
    Always
    (罕见)。
  • 遗留大量
    Always
    模式,希望无需编辑即可获得性能提升 → 保留
    Always
    模式,但设置
    $(SkipUnchangedFilesOnCopyAlways)=true