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
<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("$(InformationalVersion)")]"
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.