mcpField
M
msbuild-antipatterns

msbuild-antipatterns

github.com/dotnet测试与质量5.8k4.1k 次安装1 天前更新
代码仓库

关于 msbuild-antipatterns

用于识别项目文件中常见 MSBuild 反模式的参考目录,面向构建审查与审计。每项说明问题表现、影响及针对性修复,并区分已确认缺陷与条件性风险。

主要功能

  1. 01编号整理反模式
  2. 02说明构建与维护影响
  3. 03提供针对性转换建议
  4. 04涵盖跨平台路径处理

适用场景

  1. 01审计 MSBuild 项目文件
  2. 02检查构建维护性
  3. 03排查项目配置风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill msbuild-antipatterns

安装后在 Claude Code 中输入/msbuild-antipatterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
additional-antipatterns.md
incremental-build-inputs-outputs.md
private-assets.md

详细介绍

MSBuild 反模式目录

常见 MSBuild 反模式的编号目录。每个条目都遵循以下格式:

  • 问题特征:需要留意的内容
  • 不良影响:对构建、可维护性或正确性的影响
  • 修复方法:具体的改造方式

扫描项目文件以寻找改进点时,请使用此目录。

审查输出要求

对于审查、审计、可维护性或正确性风险相关的请求:

  1. 除非用户明确要求修复,否则不要编辑文件。
  2. 返回基于所提供文件的优先级问题,并说明影响和针对性建议。
  3. 将已确认的缺陷、条件性风险和已有的正确做法分别列出。
  4. 最终答复应保持为审查结果;不要将其替换为简短的变更摘要。

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。

评论

支持 Markdown