# Dotnet Msbuild Authoring

> Authors MSBuild targets, props, conditions, incremental builds, and Directory.Build patterns.

- Skill: `rudironsoni/dotnet-msbuild-authoring` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add rudironsoni/dotnet-msbuild-authoring`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rudironsoni/dotnet-msbuild-authoring/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools, AI & ML
- License: MIT
- Author: rudironsoni (https://skillmd.com/u/rudironsoni)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/rudironsoni/dotnet-msbuild-authoring

---


# dotnet-msbuild-authoring

Guidance for authoring MSBuild project system elements: custom targets with
`BeforeTargets`/`AfterTargets`/`DependsOnTargets`, incremental build with `Inputs`/`Outputs`, props vs targets import
ordering, items and item metadata (`Include`/`Exclude`/`Update`/`Remove`), conditions, property functions, well-known
metadata, and advanced `Directory.Build.props`/`Directory.Build.targets` patterns.

**Version assumptions:** .NET 8.0+ SDK (MSBuild 17.8+). All examples use SDK-style projects.

## Scope

- Custom targets with BeforeTargets/AfterTargets/DependsOnTargets
- Incremental build with Inputs/Outputs
- Props vs targets import ordering
- Items and item metadata (Include/Exclude/Update/Remove)
- Conditions and property functions
- Advanced Directory.Build.props/targets patterns
- MSBuild well-known metadata and item batching

## Out of scope

- Solution layout and shared configuration (CPM, .editorconfig) -- see [skill:dotnet-project-structure]
- MSBuild error interpretation and CI drift diagnosis -- see [skill:dotnet-build-analysis]
- Reading and modifying .csproj files -- see [skill:dotnet-csproj-reading]

Cross-references: [skill:dotnet-project-structure] for solution layout and basic Directory.Build.props structure,
[skill:dotnet-build-analysis] for interpreting MSBuild errors and CI drift.

---

## Custom Targets

Targets are the unit of execution in MSBuild. Each target runs a sequence of tasks and can declare ordering
relationships with other targets.

### Defining a Custom Target

````xml

<Target Name="PrintBuildInfo"
        BeforeTargets="Build">
  <Message Importance="high"
           Text="Building $(MSBuildProjectName) v$(Version) for $(TargetFramework)" />
</Target>

```text

### Ordering: BeforeTargets, AfterTargets, DependsOnTargets

Three mechanisms control target execution order:

| Mechanism | Effect | Use when |
|---|---|---|
| `BeforeTargets="X"` | Runs this target before `X` | Injecting into an existing pipeline (e.g., run before `Build`) |
| `AfterTargets="X"` | Runs this target after `X` | Post-processing (e.g., copy output after `Publish`) |
| `DependsOnTargets="A;B"` | Ensures `A` and `B` run before this target | Declaring prerequisite targets within your own target graph |

```xml

<!-- Run license check before compile -->
<Target Name="CheckLicenseHeaders"
        BeforeTargets="CoreCompile">
  <Exec Command="dotnet tool run license-check -- --verify" />
</Target>

<!-- Copy native libs after publish -->
<Target Name="CopyNativeLibs"
        AfterTargets="Publish">
  <Copy SourceFiles="@(NativeLibrary)"
        DestinationFolder="$(PublishDir)runtimes/%(NativeLibrary.RuntimeIdentifier)/native/" />
</Target>

<!-- Composite target with dependencies -->
<Target Name="FullValidation"
        DependsOnTargets="CheckLicenseHeaders;RunApiCompat">
  <Message Importance="high" Text="All validations passed." />
</Target>

```text

**Prefer `BeforeTargets`/`AfterTargets` over `DependsOnTargets`** for injecting into the standard build pipeline. `DependsOnTargets` is best for orchestrating your own custom target graph.

### Extending Existing DependsOn Lists

SDK targets expose `*DependsOn` properties for extension. Append your target name rather than replacing the list:

```xml

<PropertyGroup>
  <BuildDependsOn>$(BuildDependsOn);GenerateVersionInfo</BuildDependsOn>
</PropertyGroup>

<Target Name="GenerateVersionInfo">
  <WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"
                    Lines="[assembly: System.Reflection.AssemblyInformationalVersion(&quot;$(InformationalVersion)&quot;)]"
                    Overwrite="true" />
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)Version.g.cs" />
  </ItemGroup>
</Target>

```csharp

---

## Incremental Build with Inputs/Outputs

Targets with `Inputs` and `Outputs` only run when outputs are missing or older than inputs. This is critical for build performance.

```xml

<Target Name="GenerateEmbeddedResources"
        BeforeTargets="CoreCompile"
        Inputs="@(EmbeddedTemplate)"
        Outputs="@(EmbeddedTemplate->'$(IntermediateOutputPath)%(Filename).g.cs')">
  <Exec Command="dotnet tool run template-gen -- %(EmbeddedTemplate.Identity) -o $(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />
  <ItemGroup>
    <Compile Include="$(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />
  </ItemGroup>
</Target>

```csharp

**How incrementality works:**

1. MSBuild compares timestamps of `Inputs` items against `Outputs` items.
2. If all outputs exist and are newer than all inputs, the target is skipped entirely.
3. If any input is newer than any output, the full target runs.

**Common incrementality failures:**

- **Missing `Outputs`:** Target runs every build. Always pair `Inputs` with `Outputs`.
- **Volatile outputs:** If another target writes to the output path mid-build, timestamps reset and trigger unnecessary rebuilds.
- **Generator side effects:** Code generators that write unconditionally (even when content unchanged) break incrementality. Write to a temp file and copy only if content differs.
- **File copy timestamps:** `Copy` task with `SkipUnchangedFiles="true"` preserves timestamps; without it, every copy updates the timestamp.

---

## Props vs Targets: Import Ordering

MSBuild evaluates project files in a specific order. Understanding this is essential for correct customization.

### Evaluation Order

```text

1. Directory.Build.props          (imported by SDK early)
2. <Project Sdk="...">            (SDK props imported)
3. Explicit <Import> in project   (your .props imports)
4. Project body <PropertyGroup>,  (project-level properties)
   <ItemGroup>
5. SDK targets imported            (SDK targets)
6. Directory.Build.targets         (imported by SDK late)
7. Explicit .targets imports       (your .targets imports)

```text

### Rules

- **`.props` files** set default property values and define items. They run **before** the project body, so project-level properties can override them.
- **`.targets` files** define targets and finalize item lists. They run **after** the project body, so they see all project-level settings.

```xml

<!-- MyDefaults.props -- sets defaults, project can override -->
<Project>
  <PropertyGroup>
    <TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
    <Nullable Condition="'$(Nullable)' == ''">enable</Nullable>
  </PropertyGroup>
</Project>

```text

```xml

<!-- MyTargets.targets -- runs after project evaluation -->
<Project>
  <Target Name="ValidatePackageMetadata"
          BeforeTargets="Pack"
          Condition="'$(IsPackable)' == 'true'">
    <Error Condition="'$(Description)' == ''"
           Text="Description is required for packable projects." />
  </Target>
</Project>

```text

**Key rule:** Properties in `.props` files should use `Condition="'$(Prop)' == ''"` to allow project-level overrides. Properties in `.targets` files are evaluated last and cannot be overridden by the project.

---

## Items and Item Metadata

Items are named collections of files or values. Each item can carry metadata (key-value pairs).

### Item Operations

```xml

<ItemGroup>
  <!-- Include: add items matching a glob -->
  <Content Include="assets/**/*.png" />

  <!-- Exclude: remove items matching a pattern from the Include -->
  <Compile Include="**/*.cs" Exclude="**/*.generated.cs" />

  <!-- Update: modify metadata on existing items (does not add new items) -->
  <Content Update="assets/logo.png">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    <Pack>true</Pack>
    <PackagePath>contentFiles/any/any/</PackagePath>
  </Content>

  <!-- Remove: remove items matching a pattern from the item list -->
  <Compile Remove="legacy/**/*.cs" />
</ItemGroup>

```csharp

**SDK-style projects auto-include `*.cs` files.** Do not add a `<Compile Include="**/*.cs" />` -- it causes `NETSDK1022` duplicate items. Use `Remove` first, then `Include` for conditional compilation scenarios:

```xml

<!-- TFM-conditional compilation -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <Compile Remove="Polyfills/**/*.cs" />
</ItemGroup>

```csharp

### Well-Known Metadata

Every item has built-in metadata accessible via `%(ItemName.MetadataName)`:

| Metadata | Value | Example for `src/Models/Order.cs` |
|---|---|---|
| `%(FullPath)` | Absolute path | `/repo/src/Models/Order.cs` |
| `%(RootDir)` | Root directory | `/` |
| `%(Directory)` | Directory relative to root | `repo/src/Models/` |
| `%(Filename)` | File name without extension | `Order` |
| `%(Extension)` | File extension | `.cs` |
| `%(RecursiveDir)` | Part matched by `**` in glob | `Models/` (if glob was `src/**/*.cs`) |
| `%(Identity)` | Item spec as declared | `src/Models/Order.cs` |

### Item Metadata and Batching

Custom metadata enables per-item behavior through MSBuild batching:

```xml

<ItemGroup>
  <DbMigration Include="migrations/*.sql">
    <TargetDb>main</TargetDb>
  </DbMigration>
  <DbMigration Include="migrations/audit/*.sql">
    <TargetDb>audit</TargetDb>
  </DbMigration>
</ItemGroup>

<!-- Batching: the task runs once per unique %(TargetDb) value -->
<Target Name="RunMigrations">
  <Exec Command="sqlcmd -d %(DbMigration.TargetDb) -i %(DbMigration.Identity)"
        Condition="'@(DbMigration)' != ''" />
</Target>

```bash

`%(Metadata)` in a task attribute triggers batching. MSBuild groups items by the metadata value and invokes the task once per group.

---

## Conditions

Conditions control whether properties, items, targets, and tasks are evaluated.

### Property Conditions

```xml

<PropertyGroup>
  <!-- Default: set only if not already defined -->
  <LangVersion Condition="'$(LangVersion)' == ''">latest</LangVersion>

  <!-- TFM condition -->
  <DefineConstants Condition="$(TargetFramework.StartsWith('net8'))">$(DefineConstants);NET8_OR_GREATER</DefineConstants>

  <!-- Configuration condition -->
  <Optimize Condition="'$(Configuration)' == 'Release'">true</Optimize>

  <!-- OS condition -->
  <RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-x64</RuntimeIdentifier>
</PropertyGroup>

```text

### Item and Target Conditions

```xml

<!-- Conditional item inclusion -->
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
</ItemGroup>

<!-- Conditional target execution -->
<Target Name="SignAssembly"
        AfterTargets="Build"
        Condition="'$(Configuration)' == 'Release' AND '$(SignAssembly)' == 'true'">
  <Exec Command="signtool sign /fd SHA256 $(TargetPath)" />
</Target>

```bash

### Condition Operators

| Operator | Example |
|---|---|
| `==` / `!=` | `'$(Config)' == 'Release'` |
| `AND` / `OR` | `'$(A)' == '1' AND '$(B)' != ''` |
| `!` (negation) | `!Exists('$(OutDir)')` |
| `Exists()` | `Exists('$(SolutionDir)global.json')` |
| `HasTrailingSlash()` | `HasTrailingSlash('$(OutputPath)')` |

**Always single-quote both sides of comparisons.** `'$(Prop)' == 'value'` is correct. Unquoted comparisons fail when the property is empty.

---

## Property Functions

MSBuild properties can call .NET static methods and MSBuild intrinsic functions inline.

### .NET Static Method Calls

```xml

<PropertyGroup>
  <!-- String manipulation -->
  <NormalizedName>$([System.String]::Copy('$(PackageId)').ToLowerInvariant())</NormalizedName>

  <!-- Path combination (prefer over string concatenation) -->
  <ToolPath>$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', 'tools', 'analyzer.dll'))</ToolPath>

  <!-- GUID generation -->
  <BuildId>$([System.Guid]::NewGuid().ToString('N'))</BuildId>

  <!-- Regex replacement -->
  <CleanVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(Version)', '-.*$', ''))</CleanVersion>

  <!-- Environment variable -->
  <CiServer>$([System.Environment]::GetEnvironmentVariable('CI'))</CiServer>
</PropertyGroup>

```text

### MSBuild Intrinsic Functions

```xml

<PropertyGroup>
  <!-- OS detection -->
  <IsWindows>$([MSBuild]::IsOSPlatform('Windows'))</IsWindows>
  <IsLinux>$([MSBuild]::IsOSPlatform('Linux'))</IsLinux>
  <IsMacOS>$([MSBuild]::IsOSPlatform('OSX'))</IsMacOS>

  <!-- Arithmetic -->
  <NextVersion>$([MSBuild]::Add($(PatchVersion), 1))</NextVersion>

  <!-- Version comparison (MSBuild 17.0+) -->
  <HasModernSdk>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '8.0.100'))</HasModernSdk>

  <!-- Stable hash for deterministic output -->
  <InputHash>$([MSBuild]::StableStringHash('$(InputFile)'))</InputHash>

  <!-- Path resolution: find file by walking up directory tree -->
  <SharedPropsPath>$([MSBuild]::GetPathOfFileAbove('SharedConfig.props', '$(MSBuildProjectDirectory)'))</SharedPropsPath>

  <!-- Normalize path separators -->

## Detailed Examples

See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.

