关于 msbuild-antipatterns
用于识别项目文件中常见 MSBuild 反模式的参考目录,面向构建审查与审计。每项说明问题表现、影响及针对性修复,并区分已确认缺陷与条件性风险。
主要功能
- 01编号整理反模式
- 02说明构建与维护影响
- 03提供针对性转换建议
- 04涵盖跨平台路径处理
适用场景
- 01审计 MSBuild 项目文件
- 02检查构建维护性
- 03排查项目配置风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill msbuild-antipatterns安装后在 Claude Code 中输入/msbuild-antipatterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/additional-antipatterns.mdincremental-build-inputs-outputs.mdprivate-assets.md详细介绍
MSBuild 反模式目录
常见 MSBuild 反模式的编号目录。每个条目都遵循以下格式:
- 问题特征:需要留意的内容
- 不良影响:对构建、可维护性或正确性的影响
- 修复方法:具体的改造方式
扫描项目文件以寻找改进点时,请使用此目录。
审查输出要求
对于审查、审计、可维护性或正确性风险相关的请求:
- 除非用户明确要求修复,否则不要编辑文件。
- 返回基于所提供文件的优先级问题,并说明影响和针对性建议。
- 将已确认的缺陷、条件性风险和已有的正确做法分别列出。
- 最终答复应保持为审查结果;不要将其替换为简短的变更摘要。
AP-01:对已有内置任务的操作使用 <Exec>
问题特征:<Exec Command="mkdir ..." />、<Exec Command="copy ..." />、<Exec Command="del ..." />
不良影响:内置任务支持跨平台和增量构建,可生成结构化日志,并能以一致的方式处理错误。<Exec> 对 MSBuild 来说是不透明的。
<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:未加引号的条件表达式
问题特征:Condition="$(Foo) == Bar"——比较运算符任一侧未加引号。
不良影响:如果属性为空,或包含空格、特殊字符,条件求值可能出错,也可能引发解析错误。要确保比较可靠,MSBuild 要求字符串使用单引号括起来。
<PropertyGroup Condition="$(Configuration) == Release">
<Optimize>true</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
</PropertyGroup>
规则:始终使用单引号为 == 和 != 两侧的值都加上引号。
AP-03:硬编码绝对路径
问题特征:项目文件中出现 C:\tools\、D:\packages\、/usr/local/bin/ 等路径。
不良影响:在其他计算机、CI 环境和其他操作系统上会失效,且无法迁移。
<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:重复声明 SDK 默认值
问题特征:将属性设置为 .NET SDK 已默认提供的值。
不良影响:增加冗余内容,掩盖有意进行的重写,也让人更难识别实际自定义的内容。当新版 SDK 更改默认值时,冗余属性可能会悄然固定旧行为。
<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:在 SDK 风格项目中手动列出文件
问题特征:在 SDK 风格项目中使用 <Compile Include="File1.cs" />、<Compile Include="File2.cs" />。
不良影响:SDK 风格项目会自动匹配 **/*.cs(以及其他文件类型)。显式列出文件属于冗余操作,会造成合并冲突;如果没有将新文件添加到列表中,还可能意外遗漏。
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Services\MyService.cs" />
<Compile Include="Models\User.cs" />
</ItemGroup>
<ItemGroup>
<Compile Remove="LegacyCode\**" />
</ItemGroup>
例外情况:非 SDK 风格(旧版)项目需要显式包含文件。若要迁移,请参阅 msbuild-modernization 技能。
例外情况(F# / .fsproj):F# 编译依赖文件顺序——编译器会按顺序处理 <Compile Include> 项,而某个文件只能引用排在它之前的文件中声明的类型或模块。因此,.fsproj 文件必须按依赖顺序显式列出每个源文件(实用工具/叶模块放在顶部,Program.fs 等入口点放在底部)。如果使用 .fsi 签名文件,它必须紧挨着对应的 .fs 实现文件之前。
AP-06:对 NuGet 包使用带 HintPath 的 <Reference>
问题表现:<Reference Include="..." HintPath="..\packages\SomePackage\lib\..." />
原因:这是旧版 packages.config 模式。它不支持传递依赖、版本冲突解决或自动还原。必须提交 packages/ 文件夹,或单独还原该文件夹。
<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>
注意:对于 WindowsBase、PresentationCore 等 .NET Framework GAC 程序集,不带 HintPath 的 <Reference> 仍然有效。
AP-07:分析器/工具包缺少 PrivateAssets="all"
问题表现:<PackageReference Include="StyleCop.Analyzers" Version="..." /> 未设置 PrivateAssets="all"。
原因:如果没有 PrivateAssets="all",分析器和生成工具包会作为传递依赖传递给库的使用者。使用者会获得他们并未要求的分析器或构建时工具。
请参阅 references/private-assets.md,了解 BAD/GOOD 示例以及需要设置此项的完整包列表。
AP-08:在多个 .csproj 文件中复制粘贴属性
问题表现:相同的 <PropertyGroup> 块出现在 3 个或更多项目文件中。
原因:增加维护负担——必须在每个文件中进行更改。随着时间推移,内容会逐渐不一致。
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
有关如何组织 Directory.Build.props / Directory.Build.targets 的完整指导,请参阅 directory-build-organization 技能。
AP-09:未使用中央包管理,导致包版本分散
问题表现:<PackageReference Include="X" Version="1.2.3" />,且不同项目使用同一包的不同版本。
原因:版本逐渐偏离——不同项目使用同一包的不同版本,导致运行时不匹配、行为异常或菱形依赖冲突。
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
解决方法: 使用中央包管理。详情请参阅 https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management。
AP-10:目标过于庞大(在单个目标中包含过多内容)
问题表现:单个 <Target> 包含 50 行以上代码,执行多个互不相关的操作。
原因:无法通过增量构建跳过单个步骤,难以调试和扩展,而且目标名称会失去意义。
<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" />
</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:自定义目标缺少 Inputs 和 Outputs
问题表现:<Target Name="MyTarget" BeforeTargets="Build"> 未设置 Inputs / Outputs 属性。
原因:即使内容没有变化,该目标也会在每次构建时运行。这会破坏增量构建,并拖慢无操作构建。
请参阅 references/incremental-build-inputs-outputs.md,了解 BAD/GOOD 示例以及包含 FileWrites 注册的完整模式。
请参阅 incremental-build 技能,深入了解 Inputs/Outputs、FileWrites 和最新状态检查。
AP-12:在 .targets 中而非 .props 中设置默认值
问题表现:在 .targets 文件中的 <PropertyGroup> 内设置默认值。
原因:.targets 文件导入较晚(在项目文件之后)。等到它们设置默认值时,其他 .targets 文件可能已经使用了空值/未定义值。.props 文件会较早导入,因此才是设置默认值的正确位置。
<PropertyGroup>
<MyToolVersion>2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
<Exec Command="mytool --version $(MyToolVersion)" />
</Target>
<PropertyGroup>
<MyToolVersion Condition="'$(MyToolVersion)' == ''">2.0</MyToolVersion>
</PropertyGroup>
<Target Name="RunMyTool">
<Exec Command="mytool --version $(MyToolVersion)" />
</Target>
规则:.props = 默认值和设置(较早求值)。.targets = 构建逻辑和目标(较晚求值)。
AP-13:导入时缺少 Exists() 检查
问题模式:<Import Project="some-file.props" /> 未使用 Condition="Exists('...')" 检查。
问题原因:如果文件不存在(尚未创建、路径错误或已删除),生成会因令人困惑的错误而失败。可选导入应始终添加保护条件。
<Import Project="$(RepoRoot)eng\custom.props" />
<Import Project="$(RepoRoot)eng\custom.props" Condition="Exists('$(RepoRoot)eng\custom.props')" />
<Project Sdk="Microsoft.NET.Sdk">
例外 — 必需的导入:对于生成正确运行所必需的导入,应快速失败,不要添加保护条件。对可选导入或特定于环境的导入(例如,本地开发者覆盖设置、CI 专用设置)添加保护条件。
例外 — NuGet 包转发器:NuGet 包的每个 TFM 对应的 build/ 或 buildTransitive/ 文件夹中的 .props/.targets 文件,通常会在没有 Exists() 检查的情况下导入 buildTransitive/<tfm>/… 下的同级文件。这属于包契约:即使源代码树中不存在该相对路径,恢复的包中也保证包含目标文件。包布局通常通过以下方式生成:
- 使用自定义
.nuspec,其中包含每个 TFM 对应的<file>条目 — 例如<file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net8.0\MyAdapter.props" />— 在打包时将文件从单个源文件夹(如buildTransitive/common/)复制到每个 TFM 对应的子文件夹;或 - 在
.csproj中使用带有每个 TFM 对应的<PackagePath>的<None Update="...">/<Content Include="...">项(例如<PackagePath>buildTransitive/net8.0/</PackagePath>),每个目标 TFM 声明一次;或 - 使用 SDK 约定(例如
IncludeBuildOutput、BuildOutputTargetFolder),将生成的输出放到build/<tfm>/下。
在标记 build/ 或 buildTransitive/ 文件夹中的未添加保护条件的 <Import> 之前,应根据打包后的布局解析该导入 — 读取项目目录及其直接父目录中的每个 *.nuspec 文件(单体仓库中常见共享 nuspec;不要继续向上查找),以及 .csproj 中 <None>/<Content> 项上的任何 <PackagePath> 元数据。只有当目标路径在源代码树和推算出的包布局中均不存在时,才进行标记。dotnet-msbuild/extension-points 技能中的 Source tree vs packed layout 说明了完整的交叉检查流程。
**转发 buildTransitive/ → build/:**应通过同级的 build/*.props / build/*.targets 文件进行转发(不要直接转发到 buildMultiTargeting/);当 build/ 按 TFM 划分(build/<tfm>/)时,应包含从文件自身文件夹推导出的 TFM 片段(而不是 $(TargetFramework)),否则传递依赖的使用方会遇到 MSB4019。请参阅 extension-points 技能中的 Forwarding chain,了解相关规则和推导表达式。
AP-14:路径中的反斜杠——何时会有影响
问题表现:在旨在跨平台运行的 .props/.targets 文件中使用反斜杠作为路径分隔符。
确实会导致错误的场景(🔴 错误)——路径未经过 MSBuild 的路径规范化处理:
<Exec Command="...\tools\foo.exe ..." />中的原始 shell 字符串——会原样传递给 Unix 上的bash/sh,而它们会将\视为转义符。- CDATA 块中的反斜杠分隔路径、嵌入在
<WriteLinesToFile>写入的源文件中的路径,或为非 MSBuild 使用方(自定义脚本、响应文件、环境变量)构造的路径。 - 传递给自定义任务的路径,若这些任务直接调用操作系统文件 API,而未使用 MSBuild 路径工具。
只是风格偏好的场景(🔵 风格)——经过 MSBuild 求值器处理的路径(<Import Project="...">、由 <Copy>/<MakeDir>/<Delete> 等内置任务使用的文件路径属性、项 Include=/Exclude= 通配符):
在类 Unix 系统上,MSBuild 求值器会在解析路径之前将 \ 规范化为 /。参见 microsoft/msbuild src/Framework/FileUtilities.cs 中的 FileUtilities.MaybeAdjustFilePath 和 ConvertToUnixSlashes。因此,<Import Project="$(MSBuildThisFileDirectory)..\..\build\common.props" /> 目前在 Linux/macOS 上也能正确解析。为保持一致,仍然建议使用正斜杠,但导入不会因此失效,也不应将现有使用反斜杠的导入标记为 🔴 错误。
<Exec Command="$(MSBuildThisFileDirectory)tools\release\sign.exe $(OutputPath)" />
<Import Project="$(MSBuildThisFileDirectory)..\..\build\common.props" />
<Import Project="$(MSBuildThisFileDirectory)../../build/common.props" />
验证规则:将反斜杠路径标记为 🔴 错误之前,先问:“这个字符串会经过 MSBuild 求值器处理,还是会原样传递给非 MSBuild 使用方?” 只有第二种情况才属于正确性缺陷。
注意:$(MSBuildThisFileDirectory) 已以适合当前平台的分隔符结尾,因此 $(MSBuildThisFileDirectory)tools/mytool 在两个平台上都能正常工作。
AP-15:在多个作用域中无条件覆盖属性
问题表现:在 Directory.Build.props 和 .csproj 中都无条件设置同一属性——后写入的值会静默生效。
问题所在:难以追踪实际使用的值,也会让构建变得脆弱,并令阅读项目文件的人感到困惑。
<PropertyGroup>
<OutputPath>bin\custom\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<OutputPath>bin\other\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<OutputPath Condition="'$(OutputPath)' == ''">bin\custom\</OutputPath>
</PropertyGroup>
其他反模式(AP-16 至 AP-23)和快速参考清单,请参见 additional-antipatterns.md。