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.
1---2name: dotnet-msbuild-authoring3category: developer-experience4subcategory: msbuild5description: Authors MSBuild targets, props, conditions, incremental builds, and Directory.Build patterns.6license: MIT7targets: ['*']8tags: [foundation, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for foundation tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-msbuild-authoring2425Guidance for authoring MSBuild project system elements: custom targets with26`BeforeTargets`/`AfterTargets`/`DependsOnTargets`, incremental build with `Inputs`/`Outputs`, props vs targets import27ordering, items and item metadata (`Include`/`Exclude`/`Update`/`Remove`), conditions, property functions, well-known28metadata, and advanced `Directory.Build.props`/`Directory.Build.targets` patterns.2930**Version assumptions:** .NET 8.0+ SDK (MSBuild 17.8+). All examples use SDK-style projects.3132## Scope3334- Custom targets with BeforeTargets/AfterTargets/DependsOnTargets35- Incremental build with Inputs/Outputs36- Props vs targets import ordering37- Items and item metadata (Include/Exclude/Update/Remove)38- Conditions and property functions39- Advanced Directory.Build.props/targets patterns40- MSBuild well-known metadata and item batching4142## Out of scope4344- Solution layout and shared configuration (CPM, .editorconfig) -- see [skill:dotnet-project-structure]45- MSBuild error interpretation and CI drift diagnosis -- see [skill:dotnet-build-analysis]46- Reading and modifying .csproj files -- see [skill:dotnet-csproj-reading]4748Cross-references: [skill:dotnet-project-structure] for solution layout and basic Directory.Build.props structure,49[skill:dotnet-build-analysis] for interpreting MSBuild errors and CI drift.5051---5253## Custom Targets5455Targets are the unit of execution in MSBuild. Each target runs a sequence of tasks and can declare ordering56relationships with other targets.5758### Defining a Custom Target5960````xml6162<Target Name="PrintBuildInfo"63 BeforeTargets="Build">64 <Message Importance="high"65 Text="Building $(MSBuildProjectName) v$(Version) for $(TargetFramework)" />66</Target>6768```text6970### Ordering: BeforeTargets, AfterTargets, DependsOnTargets7172Three mechanisms control target execution order:7374| Mechanism | Effect | Use when |75|---|---|---|76| `BeforeTargets="X"` | Runs this target before `X` | Injecting into an existing pipeline (e.g., run before `Build`) |77| `AfterTargets="X"` | Runs this target after `X` | Post-processing (e.g., copy output after `Publish`) |78| `DependsOnTargets="A;B"` | Ensures `A` and `B` run before this target | Declaring prerequisite targets within your own target graph |7980```xml8182<!-- Run license check before compile -->83<Target Name="CheckLicenseHeaders"84 BeforeTargets="CoreCompile">85 <Exec Command="dotnet tool run license-check -- --verify" />86</Target>8788<!-- Copy native libs after publish -->89<Target Name="CopyNativeLibs"90 AfterTargets="Publish">91 <Copy SourceFiles="@(NativeLibrary)"92 DestinationFolder="$(PublishDir)runtimes/%(NativeLibrary.RuntimeIdentifier)/native/" />93</Target>9495<!-- Composite target with dependencies -->96<Target Name="FullValidation"97 DependsOnTargets="CheckLicenseHeaders;RunApiCompat">98 <Message Importance="high" Text="All validations passed." />99</Target>100101```text102103**Prefer `BeforeTargets`/`AfterTargets` over `DependsOnTargets`** for injecting into the standard build pipeline. `DependsOnTargets` is best for orchestrating your own custom target graph.104105### Extending Existing DependsOn Lists106107SDK targets expose `*DependsOn` properties for extension. Append your target name rather than replacing the list:108109```xml110111<PropertyGroup>112 <BuildDependsOn>$(BuildDependsOn);GenerateVersionInfo</BuildDependsOn>113</PropertyGroup>114115<Target Name="GenerateVersionInfo">116 <WriteLinesToFile File="$(IntermediateOutputPath)Version.g.cs"117 Lines="[assembly: System.Reflection.AssemblyInformationalVersion("$(InformationalVersion)")]"118 Overwrite="true" />119 <ItemGroup>120 <Compile Include="$(IntermediateOutputPath)Version.g.cs" />121 </ItemGroup>122</Target>123124```csharp125126---127128## Incremental Build with Inputs/Outputs129130Targets with `Inputs` and `Outputs` only run when outputs are missing or older than inputs. This is critical for build performance.131132```xml133134<Target Name="GenerateEmbeddedResources"135 BeforeTargets="CoreCompile"136 Inputs="@(EmbeddedTemplate)"137 Outputs="@(EmbeddedTemplate->'$(IntermediateOutputPath)%(Filename).g.cs')">138 <Exec Command="dotnet tool run template-gen -- %(EmbeddedTemplate.Identity) -o $(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />139 <ItemGroup>140 <Compile Include="$(IntermediateOutputPath)%(EmbeddedTemplate.Filename).g.cs" />141 </ItemGroup>142</Target>143144```csharp145146**How incrementality works:**1471481. MSBuild compares timestamps of `Inputs` items against `Outputs` items.1492. If all outputs exist and are newer than all inputs, the target is skipped entirely.1503. If any input is newer than any output, the full target runs.151152**Common incrementality failures:**153154- **Missing `Outputs`:** Target runs every build. Always pair `Inputs` with `Outputs`.155- **Volatile outputs:** If another target writes to the output path mid-build, timestamps reset and trigger unnecessary rebuilds.156- **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.157- **File copy timestamps:** `Copy` task with `SkipUnchangedFiles="true"` preserves timestamps; without it, every copy updates the timestamp.158159---160161## Props vs Targets: Import Ordering162163MSBuild evaluates project files in a specific order. Understanding this is essential for correct customization.164165### Evaluation Order166167```text1681691. Directory.Build.props (imported by SDK early)1702. <Project Sdk="..."> (SDK props imported)1713. Explicit <Import> in project (your .props imports)1724. Project body <PropertyGroup>, (project-level properties)173 <ItemGroup>1745. SDK targets imported (SDK targets)1756. Directory.Build.targets (imported by SDK late)1767. Explicit .targets imports (your .targets imports)177178```text179180### Rules181182- **`.props` files** set default property values and define items. They run **before** the project body, so project-level properties can override them.183- **`.targets` files** define targets and finalize item lists. They run **after** the project body, so they see all project-level settings.184185```xml186187<!-- MyDefaults.props -- sets defaults, project can override -->188<Project>189 <PropertyGroup>190 <TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>191 <Nullable Condition="'$(Nullable)' == ''">enable</Nullable>192 </PropertyGroup>193</Project>194195```text196197```xml198199<!-- MyTargets.targets -- runs after project evaluation -->200<Project>201 <Target Name="ValidatePackageMetadata"202 BeforeTargets="Pack"203 Condition="'$(IsPackable)' == 'true'">204 <Error Condition="'$(Description)' == ''"205 Text="Description is required for packable projects." />206 </Target>207</Project>208209```text210211**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.212213---214215## Items and Item Metadata216217Items are named collections of files or values. Each item can carry metadata (key-value pairs).218219### Item Operations220221```xml222223<ItemGroup>224 <!-- Include: add items matching a glob -->225 <Content Include="assets/**/*.png" />226227 <!-- Exclude: remove items matching a pattern from the Include -->228 <Compile Include="**/*.cs" Exclude="**/*.generated.cs" />229230 <!-- Update: modify metadata on existing items (does not add new items) -->231 <Content Update="assets/logo.png">232 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>233 <Pack>true</Pack>234 <PackagePath>contentFiles/any/any/</PackagePath>235 </Content>236237 <!-- Remove: remove items matching a pattern from the item list -->238 <Compile Remove="legacy/**/*.cs" />239</ItemGroup>240241```csharp242243**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:244245```xml246247<!-- TFM-conditional compilation -->248<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">249 <Compile Remove="Polyfills/**/*.cs" />250</ItemGroup>251252```csharp253254### Well-Known Metadata255256Every item has built-in metadata accessible via `%(ItemName.MetadataName)`:257258| Metadata | Value | Example for `src/Models/Order.cs` |259|---|---|---|260| `%(FullPath)` | Absolute path | `/repo/src/Models/Order.cs` |261| `%(RootDir)` | Root directory | `/` |262| `%(Directory)` | Directory relative to root | `repo/src/Models/` |263| `%(Filename)` | File name without extension | `Order` |264| `%(Extension)` | File extension | `.cs` |265| `%(RecursiveDir)` | Part matched by `**` in glob | `Models/` (if glob was `src/**/*.cs`) |266| `%(Identity)` | Item spec as declared | `src/Models/Order.cs` |267268### Item Metadata and Batching269270Custom metadata enables per-item behavior through MSBuild batching:271272```xml273274<ItemGroup>275 <DbMigration Include="migrations/*.sql">276 <TargetDb>main</TargetDb>277 </DbMigration>278 <DbMigration Include="migrations/audit/*.sql">279 <TargetDb>audit</TargetDb>280 </DbMigration>281</ItemGroup>282283<!-- Batching: the task runs once per unique %(TargetDb) value -->284<Target Name="RunMigrations">285 <Exec Command="sqlcmd -d %(DbMigration.TargetDb) -i %(DbMigration.Identity)"286 Condition="'@(DbMigration)' != ''" />287</Target>288289```bash290291`%(Metadata)` in a task attribute triggers batching. MSBuild groups items by the metadata value and invokes the task once per group.292293---294295## Conditions296297Conditions control whether properties, items, targets, and tasks are evaluated.298299### Property Conditions300301```xml302303<PropertyGroup>304 <!-- Default: set only if not already defined -->305 <LangVersion Condition="'$(LangVersion)' == ''">latest</LangVersion>306307 <!-- TFM condition -->308 <DefineConstants Condition="$(TargetFramework.StartsWith('net8'))">$(DefineConstants);NET8_OR_GREATER</DefineConstants>309310 <!-- Configuration condition -->311 <Optimize Condition="'$(Configuration)' == 'Release'">true</Optimize>312313 <!-- OS condition -->314 <RuntimeIdentifier Condition="$([MSBuild]::IsOSPlatform('Windows'))">win-x64</RuntimeIdentifier>315</PropertyGroup>316317```text318319### Item and Target Conditions320321```xml322323<!-- Conditional item inclusion -->324<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">325 <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />326</ItemGroup>327328<!-- Conditional target execution -->329<Target Name="SignAssembly"330 AfterTargets="Build"331 Condition="'$(Configuration)' == 'Release' AND '$(SignAssembly)' == 'true'">332 <Exec Command="signtool sign /fd SHA256 $(TargetPath)" />333</Target>334335```bash336337### Condition Operators338339| Operator | Example |340|---|---|341| `==` / `!=` | `'$(Config)' == 'Release'` |342| `AND` / `OR` | `'$(A)' == '1' AND '$(B)' != ''` |343| `!` (negation) | `!Exists('$(OutDir)')` |344| `Exists()` | `Exists('$(SolutionDir)global.json')` |345| `HasTrailingSlash()` | `HasTrailingSlash('$(OutputPath)')` |346347**Always single-quote both sides of comparisons.** `'$(Prop)' == 'value'` is correct. Unquoted comparisons fail when the property is empty.348349---350351## Property Functions352353MSBuild properties can call .NET static methods and MSBuild intrinsic functions inline.354355### .NET Static Method Calls356357```xml358359<PropertyGroup>360 <!-- String manipulation -->361 <NormalizedName>$([System.String]::Copy('$(PackageId)').ToLowerInvariant())</NormalizedName>362363 <!-- Path combination (prefer over string concatenation) -->364 <ToolPath>$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', 'tools', 'analyzer.dll'))</ToolPath>365366 <!-- GUID generation -->367 <BuildId>$([System.Guid]::NewGuid().ToString('N'))</BuildId>368369 <!-- Regex replacement -->370 <CleanVersion>$([System.Text.RegularExpressions.Regex]::Replace('$(Version)', '-.*$', ''))</CleanVersion>371372 <!-- Environment variable -->373 <CiServer>$([System.Environment]::GetEnvironmentVariable('CI'))</CiServer>374</PropertyGroup>375376```text377378### MSBuild Intrinsic Functions379380```xml381382<PropertyGroup>383 <!-- OS detection -->384 <IsWindows>$([MSBuild]::IsOSPlatform('Windows'))</IsWindows>385 <IsLinux>$([MSBuild]::IsOSPlatform('Linux'))</IsLinux>386 <IsMacOS>$([MSBuild]::IsOSPlatform('OSX'))</IsMacOS>387388 <!-- Arithmetic -->389 <NextVersion>$([MSBuild]::Add($(PatchVersion), 1))</NextVersion>390391 <!-- Version comparison (MSBuild 17.0+) -->392 <HasModernSdk>$([MSBuild]::VersionGreaterThanOrEquals('$(NETCoreSdkVersion)', '8.0.100'))</HasModernSdk>393394 <!-- Stable hash for deterministic output -->395 <InputHash>$([MSBuild]::StableStringHash('$(InputFile)'))</InputHash>396397 <!-- Path resolution: find file by walking up directory tree -->398 <SharedPropsPath>$([MSBuild]::GetPathOfFileAbove('SharedConfig.props', '$(MSBuildProjectDirectory)'))</SharedPropsPath>399400 <!-- Normalize path separators -->401402## Detailed Examples403404See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.