property-patterns — Claude Code skill: MSBuild properties
A skill that tracks down broken MSBuild properties using patterns from the MSBuild repo. Useful when a build picks the wrong value or fails on another OS.
npx skills add dotnet/skills --skill property-patterns -a claude-codeWhat property-patterns does
This one walks the agent through the canonical ways to define and change MSBuild properties. It reads the project and the shared files already in place, then hunts for a real defect: an unquoted condition, a default with no Condition, a wiped DefineConstants, an absolute path where a portable one belongs, a value that a later import overwrote.
Inside is a SKILL.md with XML blocks per case. Conditional defaults written as Condition="'$(Prop)' == ''", nested groups sharing one condition, semicolon appends like $(DefineConstants);MY_FEATURE, path normalization through $([MSBuild]::NormalizePath(...)) and $([System.IO.Path]::IsPathRooted(...)), guard properties that block a double import, behavior gated by $([MSBuild]::AreFeaturesEnabled('17.10')), and a walk through last-write-wins order.
One warning earns its place. In .props, $(TargetFramework) is empty for a single-targeting project until the project body is evaluated, so TFM conditions there never fire. Move that group to .targets and it works. Work without a concrete property defect is out of scope: picking which file owns a target, or reordering imports that are not actually broken, belongs to directory-build-organization.
What it helps with
- find the property a later import overwrites
- fix a default that never applies because the condition is unquoted
- append to
DefineConstantswithout losing earlier constants - replace a hardcoded absolute path in
.props - move TFM-conditioned groups out of
.propsinto.targets
Example prompts for the agent
- Configuration is always Debug in this project. Check the conditions in
Directory.Build.propsand fix the defaults. - My
TargetFrameworkcondition in.propsnever fires. Figure out why and move the group somewhere it works. - Go through the properties in our shared files and show me where
DefineConstantsgets overwritten.
How to install property-patterns
1Skills CLI (Vercel)
npx skills add dotnet/skills --skill property-patterns -a claude-code2Manually: put the files in place
~/.claude/skills/property-patterns/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/property-patterns/
1Skills CLI (Vercel)
npx skills add dotnet/skills --skill property-patterns -a codex2Manually: put the files in place
.agents/skills/property-patterns/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add dotnet/skills --skill property-patterns -a cursor2Manually: put the files in place
.cursor/skills/property-patterns/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install dotnet/skills property-patterns2Manually: put the files in place
.github/skills/property-patterns/SKILL.md1Skills CLI (Vercel)
npx skills add dotnet/skills --skill property-patterns -a gemini-cli2Platform CLI
gemini skills install https://github.com/dotnet/skills --path plugins/dotnet-msbuild/skills/property-patternsManually: put the files in place
.devin/skills/property-patterns/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add dotnet/skills --skill property-patterns -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about property-patterns
How do I install property-patterns in Claude Code?
Run npx skills add dotnet/skills --skill property-patterns -a claude-code. The skill file lands at ~/.claude/skills/property-patterns/SKILL.md.
What does property-patterns do?
It diagnoses and fixes concrete MSBuild property defects: conditions, defaults, append versus overwrite, OS and TFM checks, portable paths. It covers the project file and shared files such as Directory.Build.*.
Why does my TargetFramework condition in .props do nothing?
For a single-targeting project $(TargetFramework) is empty in .props until the project body is evaluated. Those groups belong in .targets or in the project file itself.
Source text
SKILL.md · MIT
---
name: property-patterns
description: "Diagnose and fix concrete MSBuild property defects in projects and existing shared-file hierarchies. USE FOR: conditions, defaults, append versus overwrite, last-write-wins values, OS/TFM checks, portable paths, normalization, and reviews centered on those property defects. Property defects in Directory.Build.* remain in scope, including overwritten values across parent/child imports and conditions that run before TargetFramework is set, even when the fix changes import order or moves a property group to .targets. DO NOT USE FOR: placement-only or import-only requests with no concrete property defect, such as discovering shared files or choosing which file owns a target or customization (use directory-build-organization); item operations; target structure; broad reviews without a concrete property defect; non-MSBuild work."
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.
© dotnet · MIT · dotnet/skills