msbuild-antipatterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MSBuild Anti-Pattern Catalog

MSBuild反模式目录

A numbered catalog of common MSBuild anti-patterns. Each entry follows the format:
  • Smell: What to look for
  • Why it's bad: Impact on builds, maintainability, or correctness
  • Fix: Concrete transformation
Use this catalog when scanning project files for improvements.

这是一份编号的常见MSBuild反模式目录。每个条目遵循以下格式:
  • 症状:需要排查的现象
  • 问题原因:对构建、可维护性或正确性的影响
  • 修复方案:具体的转换方法
在扫描项目文件以进行优化时可使用此目录。

AP-01:
<Exec>
for Operations That Have Built-in Tasks

AP-01:使用
<Exec>
执行已有内置任务的操作

Smell:
<Exec Command="mkdir ..." />
,
<Exec Command="copy ..." />
,
<Exec Command="del ..." />
Why it's bad: Built-in tasks are cross-platform, support incremental build, emit structured logging, and handle errors consistently.
<Exec>
is opaque to MSBuild.
xml
<!-- BAD -->
<Target Name="PrepareOutput">
  <Exec Command="mkdir $(OutputPath)logs" />
  <Exec Command="copy config.json $(OutputPath)" />
  <Exec Command="del $(IntermediateOutputPath)*.tmp" />
</Target>

<!-- GOOD -->
<Target Name="PrepareOutput">
  <MakeDir Directories="$(OutputPath)logs" />
  <Copy SourceFiles="config.json" DestinationFolder="$(OutputPath)" />
  <Delete Files="@(TempFiles)" />
</Target>
Built-in task alternatives:
Shell CommandMSBuild Task
mkdir
<MakeDir>
copy
/
cp
<Copy>
del
/
rm
<Delete>
move
/
mv
<Move>
echo text > file
<WriteLinesToFile>
touch
<Touch>
xcopy /s
<Copy>
with item globs

症状:使用
<Exec Command="mkdir ..." />
<Exec Command="copy ..." />
<Exec Command="del ..." />
问题原因:内置任务支持跨平台、增量构建,能生成结构化日志,且错误处理一致。而
<Exec>
对MSBuild来说是不透明的。
xml
<!-- 错误示例 -->
<Target Name="PrepareOutput">
  <Exec Command="mkdir $(OutputPath)logs" />
  <Exec Command="copy config.json $(OutputPath)" />
  <Exec Command="del $(IntermediateOutputPath)*.tmp" />
</Target>

<!-- 正确示例 -->
<Target Name="PrepareOutput">
  <MakeDir Directories="$(OutputPath)logs" />
  <Copy SourceFiles="config.json" DestinationFolder="$(OutputPath)" />
  <Delete Files="@(TempFiles)" />
</Target>
内置任务替代方案:
Shell命令MSBuild任务
mkdir
<MakeDir>
copy
/
cp
<Copy>
del
/
rm
<Delete>
move
/
mv
<Move>
echo text > file
<WriteLinesToFile>
touch
<Touch>
xcopy /s
使用项目通配符的
<Copy>

AP-02: Unquoted Condition Expressions

AP-02:未加引号的条件表达式

Smell:
Condition="$(Foo) == Bar"
— either side of a comparison is unquoted.
Why it's bad: If the property is empty or contains spaces/special characters, the condition evaluates incorrectly or throws a parse error. MSBuild requires single-quoted strings for reliable comparisons.
xml
<!-- BAD -->
<PropertyGroup Condition="$(Configuration) == Release">
  <Optimize>true</Optimize>
</PropertyGroup>

<!-- GOOD -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <Optimize>true</Optimize>
</PropertyGroup>
Rule: Always quote both sides of
==
and
!=
comparisons with single quotes.

症状
Condition="$(Foo) == Bar"
—— 比较的任意一侧未加引号。
问题原因:如果属性为空或包含空格/特殊字符,条件会计算错误或抛出解析异常。MSBuild要求使用单引号包裹字符串以确保可靠的比较。
xml
<!-- 错误示例 -->
<PropertyGroup Condition="$(Configuration) == Release">
  <Optimize>true</Optimize>
</PropertyGroup>

<!-- 正确示例 -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <Optimize>true</Optimize>
</PropertyGroup>
规则:在
==
!=
比较中,始终用单引号包裹两侧的内容。

AP-03: Hardcoded Absolute Paths

AP-03:硬编码绝对路径

Smell: Paths like
C:\tools\
,
D:\packages\
,
/usr/local/bin/
in project files.
Why it's bad: Breaks on other machines, CI environments, and other operating systems. Not relocatable.
xml
<!-- BAD -->
<PropertyGroup>
  <ToolPath>C:\tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="C:\repos\shared\common.props" />

<!-- GOOD -->
<PropertyGroup>
  <ToolPath>$(MSBuildThisFileDirectory)tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\common.props" />
Preferred path properties:
PropertyMeaning
$(MSBuildThisFileDirectory)
Directory of the current .props/.targets file
$(MSBuildProjectDirectory)
Directory of the .csproj
$([MSBuild]::GetDirectoryNameOfFileAbove(...))
Walk up to find a marker file
$([MSBuild]::NormalizePath(...))
Combine and normalize path segments

症状:项目文件中出现类似
C:\tools\
D:\packages\
/usr/local/bin/
的路径。
问题原因:在其他机器、CI环境或其他操作系统上会失效,不具备可移植性。
xml
<!-- 错误示例 -->
<PropertyGroup>
  <ToolPath>C:\tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="C:\repos\shared\common.props" />

<!-- 正确示例 -->
<PropertyGroup>
  <ToolPath>$(MSBuildThisFileDirectory)tools\mytool\mytool.exe</ToolPath>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\common.props" />
推荐使用的路径属性:
属性含义
$(MSBuildThisFileDirectory)
当前.props/.targets文件所在目录
$(MSBuildProjectDirectory)
.csproj文件所在目录
$([MSBuild]::GetDirectoryNameOfFileAbove(...))
向上遍历查找标记文件
$([MSBuild]::NormalizePath(...))
合并并规范化路径段

AP-04: Restating SDK Defaults

AP-04:重复声明SDK默认值

Smell: Properties set to values that the .NET SDK already provides by default.
Why it's bad: Adds noise, hides intentional overrides, and makes it harder to identify what's actually customized. When defaults change in newer SDKs, the redundant properties may silently pin old behavior.
xml
<!-- BAD: All of these are already the default -->
<PropertyGroup>
  <OutputType>Library</OutputType>
  <EnableDefaultItems>true</EnableDefaultItems>
  <EnableDefaultCompileItems>true</EnableDefaultCompileItems>
  <RootNamespace>MyLib</RootNamespace>       <!-- matches project name -->
  <AssemblyName>MyLib</AssemblyName>         <!-- matches project name -->
  <AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

<!-- GOOD: Only non-default values -->
<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

症状:将属性设置为.NET SDK已默认提供的值。
问题原因:增加冗余内容,掩盖了有意的覆盖操作,且难以识别实际自定义的配置。当新版本SDK修改默认值时,这些冗余属性可能会静默锁定旧行为。
xml
<!-- 错误示例:这些都是默认值 -->
<PropertyGroup>
  <OutputType>Library</OutputType>
  <EnableDefaultItems>true</EnableDefaultItems>
  <EnableDefaultCompileItems>true</EnableDefaultCompileItems>
  <RootNamespace>MyLib</RootNamespace>       <!-- 与项目名称一致 -->
  <AssemblyName>MyLib</AssemblyName>         <!-- 与项目名称一致 -->
  <AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
</PropertyGroup>

<!-- 正确示例:仅保留非默认值 -->
<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

AP-05: Manual File Listing in SDK-Style Projects

AP-05:在SDK风格项目中手动列出文件

Smell:
<Compile Include="File1.cs" />
,
<Compile Include="File2.cs" />
in SDK-style projects.
Why it's bad: SDK-style projects automatically glob
**/*.cs
(and other file types). Explicit listing is redundant, creates merge conflicts, and new files may be accidentally missed if not added to the list.
xml
<!-- BAD -->
<ItemGroup>
  <Compile Include="Program.cs" />
  <Compile Include="Services\MyService.cs" />
  <Compile Include="Models\User.cs" />
</ItemGroup>

<!-- GOOD: Remove entirely — SDK includes all .cs files by default.
     Only use Remove/Exclude when you need to opt out: -->
<ItemGroup>
  <Compile Remove="LegacyCode\**" />
</ItemGroup>
Exception: Non-SDK-style (legacy) projects require explicit file includes. If migrating, see
msbuild-modernization
skill.

症状:在SDK风格项目中使用
<Compile Include="File1.cs" />
<Compile Include="File2.cs" />
问题原因:SDK风格项目会自动匹配
**/*.cs
(以及其他文件类型)。显式列出文件属于冗余操作,会造成合并冲突,且新文件可能因未添加到列表中而被意外遗漏。
xml
<!-- 错误示例 -->
<ItemGroup>
  <Compile Include="Program.cs" />
  <Compile Include="Services\MyService.cs" />
  <Compile Include="Models\User.cs" />
</ItemGroup>

<!-- 正确示例:完全移除 —— SDK默认包含所有.cs文件。
     仅在需要排除文件时使用Remove/Exclude: -->
<ItemGroup>
  <Compile Remove="LegacyCode\**" />
</ItemGroup>
例外情况:非SDK风格(传统)项目需要显式包含文件。如果进行迁移,请参考
msbuild-modernization
技能。

AP-06: Using
<Reference>
with HintPath for NuGet Packages

AP-06:对NuGet包使用带HintPath的
<Reference>

Smell:
<Reference Include="..." HintPath="..\packages\SomePackage\lib\..." />
Why it's bad: This is the legacy
packages.config
pattern. It doesn't support transitive dependencies, version conflict resolution, or automatic restore. The
packages/
folder must be committed or restored separately.
xml
<!-- BAD -->
<ItemGroup>
  <Reference Include="Newtonsoft.Json">
    <HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\netstandard2.0\Newtonsoft.Json.dll</HintPath>
  </Reference>
</ItemGroup>

<!-- GOOD -->
<ItemGroup>
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
Note:
<Reference>
without HintPath is still valid for .NET Framework GAC assemblies like
WindowsBase
,
PresentationCore
, etc.

症状
<Reference Include="..." HintPath="..\packages\SomePackage\lib\..." />
问题原因:这是传统的
packages.config
模式。它不支持传递依赖、版本冲突解析或自动还原。
packages/
文件夹必须提交到版本库或单独进行还原。
xml
<!-- 错误示例 -->
<ItemGroup>
  <Reference Include="Newtonsoft.Json">
    <HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\netstandard2.0\Newtonsoft.Json.dll</HintPath>
  </Reference>
</ItemGroup>

<!-- 正确示例 -->
<ItemGroup>
  <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
注意:不带HintPath的
<Reference>
对于.NET Framework GAC程序集(如
WindowsBase
PresentationCore
等)仍然有效。

AP-07: Missing
PrivateAssets="all"
on Analyzer/Tool Packages

AP-07:分析器/工具包缺少
PrivateAssets="all"

Smell:
<PackageReference Include="StyleCop.Analyzers" Version="..." />
without
PrivateAssets="all"
.
Why it's bad: Without
PrivateAssets="all"
, analyzer and build-tool packages flow as transitive dependencies to consumers of your library. Consumers get unwanted analyzers or build-time tools they didn't ask for.
See
references/private-assets.md
for BAD/GOOD examples and the full list of packages that need this.

症状
<PackageReference Include="StyleCop.Analyzers" Version="..." />
未添加
PrivateAssets="all"
问题原因:如果没有
PrivateAssets="all"
,分析器和构建工具包会作为传递依赖流向你的库的使用者,导致使用者获得不需要的分析器或构建工具。
请查看
references/private-assets.md
获取错误/正确示例以及需要添加此属性的完整包列表。

AP-08: Copy-Pasted Properties Across Multiple .csproj Files

AP-08:在多个.csproj文件中复制粘贴属性

Smell: The same
<PropertyGroup>
block appears in 3+ project files.
Why it's bad: Maintenance burden — a change must be made in every file. Inconsistencies creep in over time.
xml
<!-- BAD: Repeated in every .csproj -->
<!-- ProjectA.csproj, ProjectB.csproj, ProjectC.csproj all have: -->
<PropertyGroup>
  <LangVersion>latest</LangVersion>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<!-- GOOD: Define once in Directory.Build.props at the repo/src root -->
<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
See
directory-build-organization
skill for full guidance on structuring
Directory.Build.props
/
Directory.Build.targets
.

症状:相同的
<PropertyGroup>
块出现在3个及以上的项目文件中。
问题原因:维护负担重 —— 每次修改都要在所有文件中进行。随着时间推移会出现不一致的情况。
xml
<!-- 错误示例:在每个.csproj中重复 -->
<!-- ProjectA.csproj、ProjectB.csproj、ProjectC.csproj都包含: -->
<PropertyGroup>
  <LangVersion>latest</LangVersion>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<!-- 正确示例:在仓库/src根目录的Directory.Build.props中定义一次 -->
<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
请查看
directory-build-organization
技能获取关于构建
Directory.Build.props
/
Directory.Build.targets
的完整指南。

AP-09: Scattered Package Versions Without Central Package Management

AP-09:未使用中央包管理导致包版本分散

Smell:
<PackageReference Include="X" Version="1.2.3" />
with different versions of the same package across projects.
Why it's bad: Version drift — different projects use different versions of the same package, leading to runtime mismatches, unexpected behavior, or diamond dependency conflicts.
xml
<!-- BAD: Version specified in each project, can drift -->
<!-- ProjectA.csproj -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<!-- ProjectB.csproj -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

症状
<PackageReference Include="X" Version="1.2.3" />
在不同项目中使用同一包的不同版本。
问题原因:版本漂移 —— 不同项目使用同一包的不同版本,导致运行时不匹配、意外行为或菱形依赖冲突。
xml
<!-- 错误示例:每个项目指定版本,容易出现漂移 -->
<!-- ProjectA.csproj -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<!-- ProjectB.csproj -->
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
修复方案: 使用中央包管理。详情请参考https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management

AP-10: Monolithic Targets (Too Much in One Target)

AP-10:单体目标(单个目标包含过多内容)

Smell: A single
<Target>
with 50+ lines doing multiple unrelated things.
Why it's bad: Can't skip individual steps via incremental build, hard to debug, hard to extend, and the target name becomes meaningless.
xml
<!-- BAD -->
<Target Name="PrepareRelease" BeforeTargets="Build">
  <WriteLinesToFile File="version.txt" Lines="$(Version)" Overwrite="true" />
  <Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" />
  <Exec Command="signtool sign /f cert.pfx $(OutputPath)*.dll" />
  <MakeDir Directories="$(OutputPath)docs" />
  <Copy SourceFiles="@(DocFiles)" DestinationFolder="$(OutputPath)docs" />
  <!-- ... 30 more lines ... -->
</Target>

<!-- GOOD: Single-responsibility targets -->
<Target Name="WriteVersionFile" BeforeTargets="CoreCompile"
        Inputs="$(MSBuildProjectFile)" Outputs="$(IntermediateOutputPath)version.txt">
  <WriteLinesToFile File="$(IntermediateOutputPath)version.txt" Lines="$(Version)" Overwrite="true" />
</Target>

<Target Name="CopyLicense" AfterTargets="Build">
  <Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>

<Target Name="SignAssemblies" AfterTargets="Build" DependsOnTargets="CopyLicense"
        Condition="'$(SignAssemblies)' == 'true'">
  <Exec Command="signtool sign /f cert.pfx %(AssemblyFiles.Identity)" />
</Target>

症状:单个
<Target>
包含50行以上代码,执行多个不相关操作。
问题原因:无法通过增量构建跳过单个步骤,难以调试和扩展,且目标名称失去意义。
xml
<!-- 错误示例 -->
<Target Name="PrepareRelease" BeforeTargets="Build">
  <WriteLinesToFile File="version.txt" Lines="$(Version)" Overwrite="true" />
  <Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" />
  <Exec Command="signtool sign /f cert.pfx $(OutputPath)*.dll" />
  <MakeDir Directories="$(OutputPath)docs" />
  <Copy SourceFiles="@(DocFiles)" DestinationFolder="$(OutputPath)docs" />
  <!-- ... 另外30行 ... -->
</Target>

<!-- 正确示例:单一职责的目标 -->
<Target Name="WriteVersionFile" BeforeTargets="CoreCompile"
        Inputs="$(MSBuildProjectFile)" Outputs="$(IntermediateOutputPath)version.txt">
  <WriteLinesToFile File="$(IntermediateOutputPath)version.txt" Lines="$(Version)" Overwrite="true" />
</Target>

<Target Name="CopyLicense" AfterTargets="Build">
  <Copy SourceFiles="LICENSE" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
</Target>

<Target Name="SignAssemblies" AfterTargets="Build" DependsOnTargets="CopyLicense"
        Condition="'$(SignAssemblies)' == 'true'">
  <Exec Command="signtool sign /f cert.pfx %(AssemblyFiles.Identity)" />
</Target>

AP-11: Custom Targets Missing
Inputs
and
Outputs

AP-11:自定义目标缺少
Inputs
Outputs

Smell:
<Target Name="MyTarget" BeforeTargets="Build">
with no
Inputs
/
Outputs
attributes.
Why it's bad: The target runs on every build, even when nothing changed. This defeats incremental build and slows down no-op builds.
See
references/incremental-build-inputs-outputs.md
for BAD/GOOD examples and the full pattern including FileWrites registration.
See
incremental-build
skill for deep guidance on Inputs/Outputs, FileWrites, and up-to-date checks.

症状
<Target Name="MyTarget" BeforeTargets="Build">
没有
Inputs
/
Outputs
属性。
问题原因:即使没有任何更改,该目标也会在每次构建时运行。这会破坏增量构建,减慢无操作构建的速度。
请查看
references/incremental-build-inputs-outputs.md
获取错误/正确示例以及包含FileWrites注册的完整模式。
请查看
incremental-build
技能获取关于Inputs/Outputs、FileWrites和最新检查的深度指南。

AP-12: Setting Defaults in .targets Instead of .props

AP-12:在.targets而非.props中设置默认值

Smell:
<PropertyGroup>
with default values inside a
.targets
file.
Why it's bad:
.targets
files are imported late (after project files). By the time they set defaults, other
.targets
files may have already used the empty/undefined value.
.props
files are imported early and are the correct place for defaults.
xml
<!-- BAD: custom.targets -->
<PropertyGroup>
  <MyToolVersion>2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
  <Exec Command="mytool --version $(MyToolVersion)" />
</Target>

<!-- GOOD: Split into .props (defaults) + .targets (logic) -->
<!-- custom.props (imported early) -->
<PropertyGroup>
  <MyToolVersion Condition="'$(MyToolVersion)' == ''">2.0</MyToolVersion>
</PropertyGroup>

<!-- custom.targets (imported late) -->
<Target Name="RunMyTool">
  <Exec Command="mytool --version $(MyToolVersion)" />
</Target>
Rule:
.props
= defaults and settings (evaluated early).
.targets
= build logic and targets (evaluated late).

症状:在
.targets
文件中包含带有默认值的
<PropertyGroup>
问题原因
.targets
文件是在项目文件之后导入的。当它们设置默认值时,其他
.targets
文件可能已经使用了空/未定义的值。
.props
文件会提前导入,是设置默认值的正确位置。
xml
<!-- 错误示例:custom.targets -->
<PropertyGroup>
  <MyToolVersion>2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
  <Exec Command="mytool --version $(MyToolVersion)" />
</Target>

<!-- 正确示例:拆分为.props(默认值) + .targets(逻辑) -->
<!-- custom.props(提前导入) -->
<PropertyGroup>
  <MyToolVersion Condition="'$(MyToolVersion)' == ''">2.0</MyToolVersion>
</PropertyGroup>

<!-- custom.targets(后期导入) -->
<Target Name="RunMyTool">
  <Exec Command="mytool --version $(MyToolVersion)" />
</Target>
规则
.props
= 默认值和设置(早期评估)。
.targets
= 构建逻辑和目标(后期评估)。

AP-13: Import Without
Exists()
Guard

AP-13:导入时未加
Exists()
保护

Smell:
<Import Project="some-file.props" />
without a
Condition="Exists('...')"
check.
Why it's bad: If the file doesn't exist (not yet created, wrong path, deleted), the build fails with a confusing error. Optional imports should always be guarded.
xml
<!-- BAD -->
<Import Project="$(RepoRoot)eng\custom.props" />

<!-- GOOD: Guard optional imports -->
<Import Project="$(RepoRoot)eng\custom.props" Condition="Exists('$(RepoRoot)eng\custom.props')" />

<!-- ALSO GOOD: Sdk attribute imports don't need guards (they're required by design) -->
<Project Sdk="Microsoft.NET.Sdk">
Exception: Imports that are required for the build to work correctly should fail fast — don't guard those. Guard imports that are optional or environment-specific (e.g., local developer overrides, CI-specific settings).

症状
<Import Project="some-file.props" />
未添加
Condition="Exists('...')"
检查。
问题原因:如果文件不存在(尚未创建、路径错误或已删除),构建会失败并显示令人困惑的错误。可选的导入应始终添加保护。
xml
<!-- 错误示例 -->
<Import Project="$(RepoRoot)eng\custom.props" />

<!-- 正确示例:保护可选导入 -->
<Import Project="$(RepoRoot)eng\custom.props" Condition="Exists('$(RepoRoot)eng\custom.props')" />

<!-- 同样正确:Sdk属性导入不需要保护(设计上是必需的) -->
<Project Sdk="Microsoft.NET.Sdk">
例外情况:对于构建正常工作必需的导入应该快速失败 —— 不要保护这些导入。仅保护可选或特定环境的导入(例如本地开发者覆盖、CI特定设置)。

AP-14: Using Backslashes in Paths (Cross-Platform Issue)

AP-14:在路径中使用反斜杠(跨平台问题)

Smell:
<Import Project="$(RepoRoot)\eng\common.props" />
with backslash separators in
.props
/
.targets
files meant to be cross-platform.
Why it's bad: Backslashes work on Windows but fail on Linux/macOS. MSBuild normalizes forward slashes on all platforms.
xml
<!-- BAD: Breaks on Linux/macOS -->
<Import Project="$(RepoRoot)\eng\common.props" />
<Content Include="assets\images\**" />

<!-- GOOD: Forward slashes work everywhere -->
<Import Project="$(RepoRoot)/eng/common.props" />
<Content Include="assets/images/**" />
Note:
$(MSBuildThisFileDirectory)
already ends with a platform-appropriate separator, so
$(MSBuildThisFileDirectory)tools/mytool
works on both platforms.

症状:在用于跨平台的.props/.targets文件中使用反斜杠分隔符,如
<Import Project="$(RepoRoot)\eng\common.props" />
问题原因:反斜杠在Windows上有效,但在Linux/macOS上会失败。MSBuild会在所有平台上规范化正斜杠。
xml
<!-- 错误示例:在Linux/macOS上会失效 -->
<Import Project="$(RepoRoot)\eng\common.props" />
<Content Include="assets\images\**" />

<!-- 正确示例:正斜杠在所有平台都有效 -->
<Import Project="$(RepoRoot)/eng/common.props" />
<Content Include="assets/images/**" />
注意
$(MSBuildThisFileDirectory)
已经以平台适配的分隔符结尾,因此
$(MSBuildThisFileDirectory)tools/mytool
在两个平台上都能正常工作。

AP-15: Unconditional Property Override in Multiple Scopes

AP-15:在多个作用域中无条件覆盖属性

Smell: A property set unconditionally in both
Directory.Build.props
and a
.csproj
— last write wins silently.
Why it's bad: Hard to trace which value is actually used. Makes the build fragile and confusing for anyone reading the project files.
xml
<!-- BAD: Directory.Build.props sets it, csproj silently overrides -->
<!-- Directory.Build.props -->
<PropertyGroup>
  <OutputPath>bin\custom\</OutputPath>
</PropertyGroup>
<!-- MyProject.csproj -->
<PropertyGroup>
  <OutputPath>bin\other\</OutputPath>
</PropertyGroup>

<!-- GOOD: Use a condition so overrides are intentional -->
<!-- Directory.Build.props -->
<PropertyGroup>
  <OutputPath Condition="'$(OutputPath)' == ''">bin\custom\</OutputPath>
</PropertyGroup>
<!-- MyProject.csproj can now intentionally override or leave the default -->

For additional anti-patterns (AP-16 through AP-21) and a quick-reference checklist, see additional-antipatterns.md.
症状:在
Directory.Build.props
.csproj
中都无条件设置同一个属性 —— 最后一次写入会静默覆盖之前的值。
问题原因:难以追踪实际使用的是哪个值。会使构建变得脆弱,给阅读项目文件的人造成困惑。
xml
<!-- 错误示例:Directory.Build.props设置了该属性,csproj静默覆盖 -->
<!-- Directory.Build.props -->
<PropertyGroup>
  <OutputPath>bin\custom\</OutputPath>
</PropertyGroup>
<!-- MyProject.csproj -->
<PropertyGroup>
  <OutputPath>bin\other\</OutputPath>
</PropertyGroup>

<!-- 正确示例:使用条件使覆盖操作更明确 -->
<!-- Directory.Build.props -->
<PropertyGroup>
  <OutputPath Condition="'$(OutputPath)' == ''">bin\custom\</OutputPath>
</PropertyGroup>
<!-- MyProject.csproj现在可以有意地覆盖或保留默认值 -->

更多反模式(AP-16至AP-21)以及快速参考检查表,请查看additional-antipatterns.md