--- name: extension-points description: "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." license: MIT --- # 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 $(MSBuildExtensionsPath)\v$(MSBuildToolsVersion)\Custom.Before.Microsoft.Common.targets ``` ### 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 $(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets ``` ## Wildcard Import Directories MSBuild imports all files in extension directories, sorted alphabetically: ```xml ``` ### Key paths | Property | Resolves to | Scope | |---|---|---| | `$(MSBuildUserExtensionsPath)` | `%APPDATA%\Microsoft\MSBuild` | Per-user | | `$(MSBuildExtensionsPath)` | MSBuild install directory | Machine-wide | | `$(MSBuildProjectExtensionsPath)` | `obj/` directory | Per-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 true true ``` ### Available control properties | Property | What it disables | |---|---| | `ImportDirectoryBuildProps` | Directory.Build.props auto-discovery | | `ImportDirectoryBuildTargets` | Directory.Build.targets auto-discovery | | `ImportProjectExtensionProps` | NuGet-generated `*.props` in obj/ | | `ImportProjectExtensionTargets` | NuGet-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//`, via `TfmSpecificPackageFile`, a per-TFM ``, or SDK conventions) while `buildMultiTargeting/` is not, a `buildTransitive//` 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 ``` ## 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` `` mappings** — copy a single source file into multiple per-TFM targets: ```xml ``` In the `` 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` `` metadata** on `` or `` items — same effect via SDK pack. Use one item per destination to keep the mapping unambiguous: ```xml ``` 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//` or `build//`. ### 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 ``` Before flagging an unguarded `` inside a `build//` or `buildTransitive//` folder: 1. Look for `*.nuspec` in the project directory and its immediate parent directory (do not walk further up). Read every `` whose `target` matches the imported path. 2. Read the `.csproj` for `` metadata on ``/`` 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 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')) ``` Only the **nearest** file is discovered. Nested hierarchies must explicitly import parents: ```xml <_ParentPropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) ``` ## Creating Your Own Extension Point ```xml $(MSBuildProjectDirectory)\MySDK.Before.targets $(MSBuildProjectDirectory)\MySDK.After.targets BeforeMySDKBuild;CoreMySDKBuild;AfterMySDKBuild ``` ## Common Pitfalls - **Missing `Exists()` on optional imports** causes build failures when files are absent. **Exception**: imports inside published `build//` and `buildTransitive//` 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.