Skip to content
Development
Skill

/extension-points

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,

From plugin
dotnet-skills
499200 skills17 agents

Claude Code

$ npx -y skills add managedcode/dotnet-skills --skill extension-points --agent claude-code

SKILL.md

extension-points.SKILL.md
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:

<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:
<PropertyGroup>
  <CustomBeforeMicrosoftCommonTargets>
    $(CustomBeforeMicrosoftCommonTargets);$(MSBuildThisFileDirectory)MyExtension.targets
  </CustomBeforeMicrosoftCommonTargets>
</PropertyGroup>

Wildcard Import Directories

MSBuild imports all files in extension directories, sorted alphabetically:

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

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:

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

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:

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):

<!-- 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

Read more
@managedcode
Ships withdotnet-skills

Stop explaining .NET to your AI. Start building. We've all been there: asking Claude to use Entity Framework, only to get EF6 patterns in a .NET 8 project. Explaining to Copilot that Blazor Server and Blazor WebAssembly aren't the same thing.

499200 skills17 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
499
Star history of managedcode/dotnet-skills on GitHub, a chart by star-history.com

Other skills on dotnet-skills.

aspnet-core

aspnet-core

Build, debug, modernize, or review ASP.NET Core applications with correct hosting, middleware, security, configuration, logging,…

aspire

aspire

Build, upgrade, and operate Aspire 13.5.x C# or TypeScript application hosts with the current CLI, AppHost, ServiceDefaults,…

avalonia

avalonia

Build and debug Avalonia 11/12 .NET applications with XAML, compiled bindings, MVVM, custom controls, desktop lifetimes, and…

azure-functions

azure-functions

Build, review, or migrate Azure Functions in .NET with correct execution model, isolated worker setup, bindings, DI, and Durable…

blazor

blazor

Build and review Blazor applications across server, WebAssembly, web app, and hybrid scenarios with correct component design,…

More skills from other plugins.

brainstorming
Auto-invoked

brainstorming

You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…

@obra@obraInstall Skill
ask-matt

ask-matt

Ask which skill or flow fits your situation. A router over the skills in this repo.

skills2w309k
@mattpocock@mattpocockInstall Skill
accessibility
Auto-invoked

accessibility

Design, implement, and audit accessible UI to WCAG 2.2 Level AA across Web, iOS, and Android — semantic ARIA roles and labels,…

ecc2w293k
@affaan-m@affaan-mInstall Skill
compiler-commit

compiler-commit

Use when you want to verify compiler changes and commit with the correct convention. Runs tests, lint, and format, then commits…

react5d252k
@facebook@facebookInstall Skill

Questions about extension-points.

Which coding agents can use extension-points?
extension-points installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.