重要前提
安装AI Skills的关键前提是:必须科学上网,且开启TUN模式,这一点至关重要,直接决定安装能否顺利完成,在此郑重提醒三遍:科学上网,科学上网,科学上网。查看完整安装教程 →
npx skills add https://github.com/dotnet/skills --skill msbuild-antipatterns一份编号整理的常见 MSBuild 反模式目录。每个条目遵循以下格式:
扫描项目文件以寻求改进时,请使用此目录。
<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>
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
内置任务替代方案:
| Shell 命令 | MSBuild 任务 |
|---|---|
mkdir | <MakeDir> |
copy / cp | <Copy> |
del / rm | <Delete> |
move / mv | <Move> |
echo text > file | <WriteLinesToFile> |
touch | <Touch> |
xcopy /s | <Copy> 配合项通配符 |
迹象:Condition="$(Foo) == Bar" —— 比较运算符的任一侧未加引号。
为何不好:如果属性为空或包含空格/特殊字符,条件将错误求值或引发解析错误。MSBuild 要求使用单引号字符串进行可靠的比较。
<!-- 错误示例 -->
<PropertyGroup Condition="$(Configuration) == Release">
<Optimize>true</Optimize>
</PropertyGroup>
<!-- 正确示例 -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>true</Optimize>
</PropertyGroup>
规则:始终用单引号将 == 和 != 比较的两侧都引起来。
迹象:项目文件中出现诸如 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(...)) | 合并并规范化路径段 |
迹象:将属性设置为 .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>
迹象:在 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>
<!-- 正确示例:完全移除 —— SDK 默认包含所有 .cs 文件。
仅在需要排除时使用 Remove/Exclude: -->
<ItemGroup>
<Compile Remove="LegacyCode\**" />
</ItemGroup>
例外情况:非 SDK 风格(旧式)项目需要显式包含文件。如果正在迁移,请参阅 msbuild-modernization 技能。
<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>
注意:不带 HintPath 的 <Reference> 对于 .NET Framework GAC 程序集(如 WindowsBase、PresentationCore 等)仍然有效。
PrivateAssets="all"迹象:<PackageReference Include="StyleCop.Analyzers" Version="..." /> 没有 PrivateAssets="all"。
为何不好:没有 PrivateAssets="all",分析器和构建工具包会作为传递依赖流向使用你的库的消费者。消费者会得到他们不想要的分析器或构建时工具。
有关错误/正确示例以及需要此设置的完整包列表,请参阅 references/private-assets.md。
迹象:相同的 <PropertyGroup> 块出现在 3 个以上的项目文件中。
为何不好:维护负担重 —— 每次更改都必须在每个文件中进行。随着时间的推移,会出现不一致。
<!-- 错误示例:在每个 .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.props / Directory.Build.targets 结构的完整指南,请参阅 directory-build-organization 技能。
迹象:<PackageReference Include="X" Version="1.2.3" /> 在不同项目中使用同一包的不同版本。
为何不好:版本漂移 —— 不同项目使用同一包的不同版本,导致运行时不匹配、意外行为或菱形依赖冲突。
<!-- 错误示例:在每个项目中指定版本,可能发生漂移 -->
<!-- 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。
迹象:一个包含 50 多行代码的 <Target>,执行多个不相关的操作。
为何不好:无法通过增量构建跳过单个步骤,难以调试,难以扩展,并且目标名称变得毫无意义。
<!-- 错误示例 -->
<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>
Inputs 和 Outputs迹象:<Target Name="MyTarget" BeforeTargets="Build"> 没有 Inputs / Outputs 属性。
为何不好:该目标在每次构建时都会运行,即使没有任何更改。这破坏了增量构建,并减慢了无操作构建的速度。
有关错误/正确示例以及包括 FileWrites 注册在内的完整模式,请参阅 references/incremental-build-inputs-outputs.md。
有关 Inputs/Outputs、FileWrites 和最新检查的深入指导,请参阅 incremental-build 技能。
迹象:在 .targets 文件中包含默认值的 <PropertyGroup>。
为何不好:.targets 文件导入较晚(在项目文件之后)。当它们设置默认值时,其他 .targets 文件可能已经使用了空/未定义的值。.props 文件导入较早,是设置默认值的正确位置。
<!-- 错误示例: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 = 构建逻辑和目标(后期求值)。
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')" />
<!-- 同样正确:Sdk 属性导入不需要保护(设计上就是必需的) -->
<Project Sdk="Microsoft.NET.Sdk">
例外情况:对于构建正常工作所必需的导入,应该快速失败 —— 不要保护这些。保护那些可选的或特定于环境的导入(例如,本地开发者覆盖、CI 特定设置)。
迹象:在旨在跨平台的 .props/.targets 文件中出现 <Import Project="$(RepoRoot)\eng\common.props" /> 等带反斜杠分隔符的路径。
为何不好:反斜杠在 Windows 上有效,但在 Linux/macOS 上会失败。MSBuild 在所有平台上都会规范化正斜杠。
<!-- 错误示例:在 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 在两个平台上都有效。
迹象:一个属性在 Directory.Build.props 和 .csproj 中都被无条件设置 —— 最后写入者静默胜出。
为何不好:难以追踪实际使用的是哪个值。使得构建脆弱,并且让阅读项目文件的任何人感到困惑。
<!-- 错误示例: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。
每周安装次数
72
代码仓库
GitHub 星标数
725
首次出现
2026年3月10日
安全审计
安装于
github-copilot69
opencode69
kimi-cli67
gemini-cli67
amp67
codex67
A numbered catalog of common MSBuild anti-patterns. Each entry follows the format:
Use this catalog when scanning project files for improvements.
<Exec> for Operations That Have Built-in TasksSmell : <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.
<!-- 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 Command | MSBuild Task |
|---|---|
mkdir | <MakeDir> |
copy / cp | <Copy> |
del / rm | <Delete> |
move / |
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.
<!-- 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.
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.
<!-- 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:
| Property | Meaning |
|---|---|
$(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 |
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.
<!-- 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>
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.
<!-- 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.
<Reference> with HintPath for NuGet PackagesSmell : <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.
<!-- 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.
PrivateAssets="all" on Analyzer/Tool PackagesSmell : <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.
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.
<!-- 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.
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.
<!-- 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" />
Fix: Use Central Package Management. See https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management for details.
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.
<!-- 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>
Inputs and OutputsSmell : <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.
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.
<!-- 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).
Exists() GuardSmell : <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.
<!-- 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).
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.
<!-- 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.
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.
<!-- 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.
Weekly Installs
72
Repository
GitHub Stars
725
First Seen
Mar 10, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot69
opencode69
kimi-cli67
gemini-cli67
amp67
codex67
Angular编译器CLI (ngtsc) 架构详解:Ivy编译、模板类型检查与AOT
438 周安装
mv<Move> |
echo text > file | <WriteLinesToFile> |
touch | <Touch> |
xcopy /s | <Copy> with item globs |