property-patterns
MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing-slash handling, TFM detection helpers, and evaluation order. USE FOR: diagnosing and fixing property definition issues and shared-property anti-patterns in .props/.
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-msbuild/skills/property-patterns
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
MSBuild Property Patterns
Canonical property definition and manipulation patterns from the MSBuild repository.
Conditional Defaults — The Foundational Pattern
Set a property only if not already set, allowing callers to override:
<PropertyGroup>
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<Platform Condition="'$(Platform)' == ''">AnyCPU</Platform>
<BuildInParallel Condition="'$(BuildInParallel)' == ''">true</BuildInParallel>
</PropertyGroup>
Rules
- Always quote both sides:
'$(Prop)' == '' - In
.props: creates overridable defaults. In.targets: creates fallbacks. - Properties without the condition cannot be overridden by earlier imports.
Nested Conditional Groups
Group related properties under a shared condition:
<PropertyGroup Condition="$(TargetFramework.StartsWith('net4'))">
<DefineConstants>$(DefineConstants);FEATURE_APARTMENT_STATE</DefineConstants>
<DefineConstants>$(DefineConstants);FEATURE_APM</DefineConstants>
<FeatureAppDomain>true</FeatureAppDomain>
</PropertyGroup>
<PropertyGroup Condition="'$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))' == '.NETCoreApp'">
<NetCoreBuild>true</NetCoreBuild>
<DefineConstants>$(DefineConstants);RUNTIME_TYPE_NETCORE</DefineConstants>
</PropertyGroup>
Use the outer Condition on PropertyGroup to avoid repeating the same condition on every property.
Warning:
$(TargetFramework)is empty in.propsfiles for single-targeting projects until the project body is evaluated. PlaceTargetFramework-conditioned property groups in.targetsfiles (or the project file itself), where the value is always available.
Composition — Semicolon Concatenation
Properties that hold lists use semicolons. Always include the existing value when appending:
<PropertyGroup>
<DefineConstants>$(DefineConstants);MY_FEATURE</DefineConstants>
<NoWarn>$(NoWarn);NU5131;IDE0005</NoWarn>
<LibraryTargetFrameworks>$(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild);netstandard2.0</LibraryTargetFrameworks>
</PropertyGroup>
Path Normalization and Trailing Slashes
<!-- Ensure trailing slash on directories -->
<PropertyGroup>
<OutDir Condition="'$(OutDir)' != '' and !HasTrailingSlash('$(OutDir)')">$(OutDir)\</OutDir>
</PropertyGroup>
<!-- Normalize paths for cross-platform -->
<PropertyGroup>
<TargetRefPath>$([MSBuild]::NormalizePath('$(TargetDir)', 'ref', '$(TargetFileName)'))</TargetRefPath>
</PropertyGroup>
<!-- Make relative path absolute -->
<PropertyGroup>
<MSBuildProjectExtensionsPath
Condition="'$([System.IO.Path]::IsPathRooted('$(MSBuildProjectExtensionsPath)'))' == 'false'">
$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))
</MSBuildProjectExtensionsPath>
</PropertyGroup>
Preferred path functions
| Function | Purpose |
|---|---|
$([MSBuild]::NormalizePath(...)) |
Combine and normalize (cross-platform) |
$([System.IO.Path]::Combine(...)) |
Combine path segments |
$([System.IO.Path]::IsPathRooted(...)) |
Check if absolute |
HasTrailingSlash(...) |
Check for trailing slash |
$([MSBuild]::GetDirectoryNameOfFileAbove(...)) |
Walk up directory tree |
$(MSBuildThisFileDirectory) |
Directory of current file |
Target Framework Detection Helpers
<!-- Get TFM identifier -->
<PropertyGroup Condition="'$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))' == '.NETCoreApp'">
<NetCoreBuild>true</NetCoreBuild>
</PropertyGroup>
<!-- Check TFM compatibility -->
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net472'))">
<UseFrozenVersions>true</UseFrozenVersions>
</PropertyGroup>
<!-- OS detection -->
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('windows'))">
<DefineConstants>$(DefineConstants);TEST_ISWINDOWS</DefineConstants>
</PropertyGroup>
Guard Properties
Mark that a file has been imported to prevent double-imports:
<!-- At the end of MySDK.props -->
<PropertyGroup>
<MySDKPropsImported>true</MySDKPropsImported>
</PropertyGroup>
<!-- At the top of MySDK.targets -->
<Import Project="MySDK.props" Condition="'$(MySDKPropsImported)' != 'true'" />
Feature Gating by MSBuild Version
<PropertyGroup Condition="$([MSBuild]::AreFeaturesEnabled('17.10'))">
<UseNewBehavior>true</UseNewBehavior>
</PropertyGroup>
Fallback Chains
Set via primary source first, then fall back:
<PropertyGroup>
<TlbExpPath>$([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToDotNetFrameworkSdkFile('tlbexp.exe'))</TlbExpPath>
<TlbExpPath Condition="'$(TlbExpPath)' == ''">$(_NetFxToolsDir)TlbExp.exe</TlbExpPath>
</PropertyGroup>
Last Write Wins — Evaluation Order
MSBuild evaluates properties top-to-bottom. The last assignment wins:
<!-- File 1 (imported first) -->
<MyProp>value1</MyProp> <!-- set to value1 -->
<!-- File 2 (imported second) -->
<MyProp>value2</MyProp> <!-- overwritten to value2 -->
<!-- File 3 (imported third) -->
<MyProp Condition="'$(MyProp)' == ''">value3</MyProp> <!-- NOT set — already value2 -->
Properties in .targets (imported late) override properties in .props (imported early) and the project file.
Common Pitfalls
- Unquoted conditions (
$(X)==true) fail when the property is empty. Always quote both sides. - Overwriting DefineConstants (
<DefineConstants>MY_CONST</DefineConstants>) drops all prior constants. Always append with$(DefineConstants);. - Hardcoded absolute paths break portability. Use
$(MSBuildThisFileDirectory)or$([MSBuild]::NormalizePath(...)). - Missing
Conditionon defaults makes properties non-overridable. AddCondition="'$(Prop)' == ''"for values meant to be defaults.
Files (skills)
-
SKILL.md 6.6 KB
--- name: property-patterns description: "MSBuild property definition patterns: conditional defaults, composition/concatenation, path normalization, trailing-slash handling, TFM detection helpers, and evaluation order. USE FOR: diagnosing and fixing property definition issues and shared-property anti-patterns in .props/.csproj; DefineConstants or NoWarn overwritten instead of appended; unconditional assignments that block project-level overrides; unquoted conditions that fail on empty properties; hardcoded paths that break cross-platform builds; setting overridable defaults; property evaluation order and last-write-wins semantics. DO NOT USE FOR: props vs targets placement (use directory-build-organization), item operations (use item-management), target structure (use target-authoring), general anti-patterns (use msbuild-antipatterns), non-MSBuild build systems." license: MIT --- # MSBuild Property Patterns Canonical property definition and manipulation patterns from the MSBuild repository. ## Conditional Defaults — The Foundational Pattern Set a property **only if not already set**, allowing callers to override: ```xml <PropertyGroup> <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration> <Platform Condition="'$(Platform)' == ''">AnyCPU</Platform> <BuildInParallel Condition="'$(BuildInParallel)' == ''">true</BuildInParallel> </PropertyGroup> ``` ### Rules - Always quote both sides: `'$(Prop)' == ''` - In `.props`: creates overridable defaults. In `.targets`: creates fallbacks. - Properties without the condition **cannot be overridden** by earlier imports. ## Nested Conditional Groups Group related properties under a shared condition: ```xml <PropertyGroup Condition="$(TargetFramework.StartsWith('net4'))"> <DefineConstants>$(DefineConstants);FEATURE_APARTMENT_STATE</DefineConstants> <DefineConstants>$(DefineConstants);FEATURE_APM</DefineConstants> <FeatureAppDomain>true</FeatureAppDomain> </PropertyGroup> <PropertyGroup Condition="'$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))' == '.NETCoreApp'"> <NetCoreBuild>true</NetCoreBuild> <DefineConstants>$(DefineConstants);RUNTIME_TYPE_NETCORE</DefineConstants> </PropertyGroup> ``` Use the outer `Condition` on `PropertyGroup` to avoid repeating the same condition on every property. > **Warning:** `$(TargetFramework)` is empty in `.props` files for single-targeting projects until the project body is evaluated. Place `TargetFramework`-conditioned property groups in `.targets` files (or the project file itself), where the value is always available. ## Composition — Semicolon Concatenation Properties that hold lists use semicolons. Always include the existing value when appending: ```xml <PropertyGroup> <DefineConstants>$(DefineConstants);MY_FEATURE</DefineConstants> <NoWarn>$(NoWarn);NU5131;IDE0005</NoWarn> <LibraryTargetFrameworks>$(FullFrameworkTFM);$(LatestDotNetCoreForMSBuild);netstandard2.0</LibraryTargetFrameworks> </PropertyGroup> ``` ## Path Normalization and Trailing Slashes ```xml <!-- Ensure trailing slash on directories --> <PropertyGroup> <OutDir Condition="'$(OutDir)' != '' and !HasTrailingSlash('$(OutDir)')">$(OutDir)\</OutDir> </PropertyGroup> <!-- Normalize paths for cross-platform --> <PropertyGroup> <TargetRefPath>$([MSBuild]::NormalizePath('$(TargetDir)', 'ref', '$(TargetFileName)'))</TargetRefPath> </PropertyGroup> <!-- Make relative path absolute --> <PropertyGroup> <MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted('$(MSBuildProjectExtensionsPath)'))' == 'false'"> $([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)')) </MSBuildProjectExtensionsPath> </PropertyGroup> ``` ### Preferred path functions | Function | Purpose | |---|---| | `$([MSBuild]::NormalizePath(...))` | Combine and normalize (cross-platform) | | `$([System.IO.Path]::Combine(...))` | Combine path segments | | `$([System.IO.Path]::IsPathRooted(...))` | Check if absolute | | `HasTrailingSlash(...)` | Check for trailing slash | | `$([MSBuild]::GetDirectoryNameOfFileAbove(...))` | Walk up directory tree | | `$(MSBuildThisFileDirectory)` | Directory of current file | ## Target Framework Detection Helpers ```xml <!-- Get TFM identifier --> <PropertyGroup Condition="'$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))' == '.NETCoreApp'"> <NetCoreBuild>true</NetCoreBuild> </PropertyGroup> <!-- Check TFM compatibility --> <PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net472'))"> <UseFrozenVersions>true</UseFrozenVersions> </PropertyGroup> <!-- OS detection --> <PropertyGroup Condition="$([MSBuild]::IsOSPlatform('windows'))"> <DefineConstants>$(DefineConstants);TEST_ISWINDOWS</DefineConstants> </PropertyGroup> ``` ## Guard Properties Mark that a file has been imported to prevent double-imports: ```xml <!-- At the end of MySDK.props --> <PropertyGroup> <MySDKPropsImported>true</MySDKPropsImported> </PropertyGroup> <!-- At the top of MySDK.targets --> <Import Project="MySDK.props" Condition="'$(MySDKPropsImported)' != 'true'" /> ``` ## Feature Gating by MSBuild Version ```xml <PropertyGroup Condition="$([MSBuild]::AreFeaturesEnabled('17.10'))"> <UseNewBehavior>true</UseNewBehavior> </PropertyGroup> ``` ## Fallback Chains Set via primary source first, then fall back: ```xml <PropertyGroup> <TlbExpPath>$([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToDotNetFrameworkSdkFile('tlbexp.exe'))</TlbExpPath> <TlbExpPath Condition="'$(TlbExpPath)' == ''">$(_NetFxToolsDir)TlbExp.exe</TlbExpPath> </PropertyGroup> ``` ## Last Write Wins — Evaluation Order MSBuild evaluates properties top-to-bottom. The last assignment wins: ```xml <!-- File 1 (imported first) --> <MyProp>value1</MyProp> <!-- set to value1 --> <!-- File 2 (imported second) --> <MyProp>value2</MyProp> <!-- overwritten to value2 --> <!-- File 3 (imported third) --> <MyProp Condition="'$(MyProp)' == ''">value3</MyProp> <!-- NOT set — already value2 --> ``` Properties in `.targets` (imported late) override properties in `.props` (imported early) and the project file. ## Common Pitfalls - **Unquoted conditions** (`$(X)==true`) fail when the property is empty. Always quote both sides. - **Overwriting DefineConstants** (`<DefineConstants>MY_CONST</DefineConstants>`) drops all prior constants. Always append with `$(DefineConstants);`. - **Hardcoded absolute paths** break portability. Use `$(MSBuildThisFileDirectory)` or `$([MSBuild]::NormalizePath(...))`. - **Missing `Condition` on defaults** makes properties non-overridable. Add `Condition="'$(Prop)' == ''"` for values meant to be defaults.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.