Extension Points

作者 dotnet0608d8924cd3MIT5.5K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫今天更新

Own MSBuild import and hook discovery. USE FOR: CustomBefore/CustomAfter hooks, ordered wildcard and NuGet auto-imports, control properties, build/buildTransitive packed layout, package ID and file-name matching, per-TFM forwarders, and tracing why assets or hooks are missing, broken, or replaced. An import guard remains in scope when it is one defect in a broader hook/import flow. DO NOT USE for a focused safety verdict on whether one specific Import needs Exists or is an intentionally unguarded package contract; use msbuild-antipatterns. NEVER INVOKE when imports and hook placement already work and the request is only target Inputs/Outputs, incremental skipping, or FileWrites clean tracking; use incremental-build or target-authoring. Exclude props-versus-targets placement and non-MSBuild systems.

AI 產生的概覽

關於 MSBuild 匯入與掛鉤探索機制的參考指南,涵蓋 CustomBefore/CustomAfter 掛鉤、萬用字元與 NuGet 自動匯入,以及封裝配置。

功能
此技能提供參考文件,說明 MSBuild 管線如何為 SDK、NuGet 套件、程式碼儲存庫與使用者提供擴充點。內容涵蓋 CustomBefore/CustomAfter 掛鉤、萬用字元匯入目錄、匯入開關控制屬性、NuGet 的 build/buildTransitive 套件配置、依目標 Framework(TFM)的轉送器、Directory.Build 探索機制,以及如何建立自己的擴充點。此外也列出常見陷阱,並提供審閱者如何區分原始碼樹配置與封裝後配置的指引。
適用情境
適用於診斷或設計 MSBuild 匯入與掛鉤流程的情境,例如掛鉤與資產遺失、損毀或被取代、NuGet 建置擴充封裝,或依 TFM 的轉送器。不適用於對單一 Import 做聚焦的安全性判定,也不適用於目標的 Inputs/Outputs、增量略過或 FileWrites 追蹤。
執行需求
不需要指令碼或執行階段相依性,僅為純說明性參考內容。所述主題涉及 MSBuild 與 NuGet 建置擴充套件,但技能本身除代理程式外不需要任何其他條件。

MSBuild Extension Points

How the MSBuild pipeline provides hooks for SDKs, NuGet packages, repos, and users to inject custom logic.

CustomBefore / CustomAfter Hooks

Every major .targets file defines import hooks:

xml
<PropertyGroup>  <CustomBeforeMicrosoftCommonTargets Condition="'$(CustomBeforeMicrosoftCommonTargets)' == ''">    $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets  </CustomBeforeMicrosoftCommonTargets></PropertyGroup>
<Import Project="$(CustomBeforeMicrosoftCommonTargets)"    Condition="'$(CustomBeforeMicrosoftCommonTargets)' != '' and Exists('$(CustomBeforeMicrosoftCommonTargets)')"/><!-- ... core targets ... --><Import Project="$(CustomAfterMicrosoftCommonTargets)"    Condition="'$(CustomAfterMicrosoftCommonTargets)' != '' and Exists('$(CustomAfterMicrosoftCommonTargets)')"/>

Rules

  • Default path includes version (v$(MSBuildToolsVersion)) for side-by-side installations.
  • Always check Exists(). The file may not be present on every machine.
  • Append to the property (don't overwrite) to chain multiple hooks:
xml
<PropertyGroup>  <CustomBeforeMicrosoftCommonTargets>    $(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets  </CustomBeforeMicrosoftCommonTargets></PropertyGroup>

Wildcard Import Directories

MSBuild imports all files in extension directories, sorted alphabetically:

xml
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore\*"    Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == 'true'               and Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Imports\Microsoft.Common.props\ImportBefore')" />

Key paths

PropertyResolves toScope
$(MSBuildUserExtensionsPath)%APPDATA%\Microsoft\MSBuildPer-user
$(MSBuildExtensionsPath)MSBuild install directoryMachine-wide
$(MSBuildProjectExtensionsPath)obj/ directoryPer-project (NuGet)

Name files with numeric prefixes for ordering: 01-first.props, 02-second.props.

Import Gating — Control Properties

Every wildcard import is gated by a boolean property:

xml
<PropertyGroup>  <ImportByWildcardBeforeMicrosoftCommonProps      Condition="'$(ImportByWildcardBeforeMicrosoftCommonProps)' == ''">true</ImportByWildcardBeforeMicrosoftCommonProps>  <ImportDirectoryBuildProps      Condition="'$(ImportDirectoryBuildProps)' == ''">true</ImportDirectoryBuildProps></PropertyGroup>

Available control properties

PropertyWhat it disables
ImportDirectoryBuildPropsDirectory.Build.props auto-discovery
ImportDirectoryBuildTargetsDirectory.Build.targets auto-discovery
ImportProjectExtensionPropsNuGet-generated *.props in obj/
ImportProjectExtensionTargetsNuGet-generated *.targets in obj/
ImportByWildcardBefore*Machine-level ImportBefore extensions
ImportByWildcardAfter*Machine-level ImportAfter extensions

NuGet Package Build Extension Layout

NuGet packages inject build logic via build/ or buildTransitive/ folders:

text
MyPackage/  build/    MyPackage.props      ← imported via *.props wildcard    MyPackage.targets    ← imported via *.targets wildcard  buildTransitive/    MyPackage.props      ← imported by transitive consumers    MyPackage.targets

Rules

  • File names must match the package ID exactly.
  • build/ affects direct consumers only. buildTransitive/ affects the entire dependency chain.
  • Props are imported early (before the project), targets are imported late (after the project).

Forwarding chain: buildTransitive/ → build/ → shared

Forward buildTransitive/*.props and buildTransitive/*.targets through their sibling build/*.props / build/*.targets files (chain buildTransitive → build → shared) instead of importing buildMultiTargeting/ directly. This keeps build/ as the single source of truth with a clear ownership chain, so transitive consumers stay in sync with direct consumers instead of the two layouts drifting apart.

When build/ is packed per-TFM (build/<tfm>/, via TfmSpecificPackageFile, a per-TFM <PackagePath>, or SDK conventions) while buildMultiTargeting/ is not, a buildTransitive/<tfm>/ forwarder must include the TFM segment — dropping it resolves to a non-existent package-root build/MyPackage.props and fails transitive consumers with MSB4019. Derive the segment from the file's own folder, never $(TargetFramework) (NuGet nearest-match can serve a net10.0 consumer the net9.0 folder, so $(TargetFramework) may name a folder that was never restored):

xml
<!-- buildTransitive/<tfm>/MyPackage.props --><Import Project="$(MSBuildThisFileDirectory)..\..\build\$([System.IO.Path]::GetFileName($([System.IO.Path]::GetDirectoryName('$(MSBuildThisFileDirectory)'))))\MyPackage.props" />

Source Tree vs Packed Layout

When reviewing a NuGet build-extension package, the source layout in the repository can legitimately differ from the packed layout inside the produced .nupkg. This is a common source of false-positive "import points at a missing file" findings.

Three packaging mechanisms reshape the layout at pack time:

  1. .nuspec <file src=… target=…> mappings — copy a single source file into multiple per-TFM targets:

    xml
    <!-- Source tree has ONE shared file:       buildTransitive\common\MyAdapter.props     Pack rewrites it to per-TFM targets inside the .nupkg:       buildTransitive\net462\MyAdapter.props       buildTransitive\net8.0\MyAdapter.props       buildTransitive\net9.0\MyAdapter.props --><files>  <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net462\MyAdapter.props" />  <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net8.0\MyAdapter.props" />  <file src="buildTransitive\common\MyAdapter.props" target="buildTransitive\net9.0\MyAdapter.props" /></files>

    In the <file> element, a target ending in \ is treated as a folder (filename preserved from src); a target ending in a filename renames the file.

  2. .csproj <PackagePath> metadata on <None Update=…> or <Content Include=…> items — same effect via SDK pack. Use one item per destination to keep the mapping unambiguous:

    xml
    <ItemGroup>  <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net8.0\MyAdapter.props" />  <None Include="buildTransitive\common\MyAdapter.props" Pack="true" PackagePath="buildTransitive\net9.0\MyAdapter.props" /></ItemGroup>

    NuGet/SDK pack also accepts a semicolon-separated list (PackagePath="buildTransitive\net8.0\;buildTransitive\net9.0\") to fan one source out to multiple destinations, but the multi-item form above is harder to misread.

  3. SDK conventions — IncludeBuildOutput, BuildOutputTargetFolder, IncludeContentInPack automatically place built outputs under lib/<tfm>/ or build/<tfm>/.

Implication for reviewers

A forwarder like the following inside a packed build/net462/ folder is not a "missing-file" bug, even if the source tree has no buildTransitive/net462/ directory:

xml
<!-- In packed build/net462/MyAdapter.props --><Project>  <Import Project="$(MSBuildThisFileDirectory)..\..\buildTransitive\net462\MyAdapter.props" /></Project>

Before flagging an unguarded <Import> inside a build/<tfm>/ or buildTransitive/<tfm>/ folder:

  1. Look for *.nuspec in the project directory and its immediate parent directory (do not walk further up). Read every <file target=…> whose target matches the imported path.
  2. Read the .csproj for <PackagePath> metadata on <None>/<Content> items.
  3. Only flag the import if the target path is missing from both the source tree and the projected package layout.

See also msbuild-antipatterns AP-13 ("NuGet package forwarders" exception).

Import Guard Pattern

The .targets file ensures .props was imported using a guard property:

xml
<!-- End of Microsoft.Common.props --><PropertyGroup>  <MicrosoftCommonPropsHasBeenImported>true</MicrosoftCommonPropsHasBeenImported></PropertyGroup>
<!-- Top of Microsoft.Common.CurrentVersion.targets --><Import Project="Microsoft.Common.props"    Condition="'$(MicrosoftCommonPropsHasBeenImported)' != 'true'" />

This handles projects that only import .targets.

Directory.Build Discovery

MSBuild walks up the directory tree to find the nearest Directory.Build.props:

xml
<_DirectoryBuildPropsBasePath>  $([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'Directory.Build.props'))</_DirectoryBuildPropsBasePath>

Only the nearest file is discovered. Nested hierarchies must explicitly import parents:

xml
<!-- src/Directory.Build.props --><PropertyGroup>  <_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))</_ParentPropsPath></PropertyGroup><Import Project="$(_ParentPropsPath)" Condition="'$(_ParentPropsPath)' != ''" />

Creating Your Own Extension Point

xml
<!-- MySDK.targets --><Project>  <Import Project="MySDK.props" Condition="'$(MySDKPropsImported)' != 'true'" />
  <PropertyGroup>    <CustomBeforeMySDK Condition="'$(CustomBeforeMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.Before.targets</CustomBeforeMySDK>    <CustomAfterMySDK Condition="'$(CustomAfterMySDK)' == ''">$(MSBuildProjectDirectory)\MySDK.After.targets</CustomAfterMySDK>  </PropertyGroup>
  <Import Project="$(CustomBeforeMySDK)" Condition="Exists('$(CustomBeforeMySDK)')" />
  <PropertyGroup>    <MySDKBuildDependsOn>BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild</MySDKBuildDependsOn>  </PropertyGroup>  <Target Name="MySDKBuild" DependsOnTargets="$(MySDKBuildDependsOn)" />  <Target Name="BeforeMySDKBuild" />  <Target Name="AfterMySDKBuild" />  <Target Name="CoreMySDKBuild">    <!-- implementation -->  </Target>
  <Import Project="$(CustomAfterMySDK)" Condition="Exists('$(CustomAfterMySDK)')" /></Project>

Common Pitfalls

  • Missing Exists() on optional imports causes build failures when files are absent. Exception: imports inside published build/<tfm>/ and buildTransitive/<tfm>/ folders of a NuGet package are a package contract — the target is guaranteed by the packed layout (see "Source Tree vs Packed Layout" above). Don't guard them and don't flag them.
  • Overwriting Custom properties* drops prior hooks. Append with ; separator.
  • NuGet package file names not matching package ID silently skips the import.
  • Nested Directory.Build.props without parent import loses repo-root settings.

來源與署名

來源:dotnet/skills位於plugins/dotnet-msbuild/skills/extension-points提交0608d89

授權條款: MIT

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架