Diagnose and fix MSBuild property defects including conditional defaults, path normalization, and evaluation order issues.
- GitHub stars
- 5,942
- Files
- 1
- Last commit
- Nov 11, 2026
- License
- MIT
- Allowed tools
- —
What does the property-patterns skill do?
property-patterns is an agent skill. Diagnose and fix MSBuild property defects including conditional defaults, path normalization, and evaluation order issues.
Summary generated from the skill's SKILL.md.
About the property-patterns skill
Fixing MSBuild property defects in complex project hierarchies is what this skill enables. It is designed for developers managing shared-file structures, such as Directory.Build files, who need to resolve issues with conditional defaults, path normalization, and evaluation order. An agent should load this skill when a user needs to diagnose why a property is being overwritten incorrectly or why a path is not behaving predictably across different operating systems.
The tool provides specific patterns for handling semicolon-separated lists, implementing fallback chains, and using guard properties to prevent double-imports. It relies on MSBuild-specific functions like NormalizePath, IsPathRooted, and GetTargetFrameworkIdentifier to ensure portability. While it excels at fixing concrete property defects, it is not intended for general target structure reviews or item operations. It specifically addresses problems like unquoted conditions, accidental overwriting of DefineConstants, and the timing of TargetFramework evaluation in .props versus .targets files.
What an agent can do with it
- 1Fixing non-overridable default propertiesEnsure that default values in a props file can be overridden by the project file by adding the correct conditional check.
- 2Resolving TargetFramework evaluation issuesMove property groups that rely on the target framework from .props files to .targets files to ensure the value is available.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the property-patterns skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the property-patterns skill in Plugin
/plugin marketplace add dotnet/skills /plugin install dotnet-msbuild@dotnet-agent-skills
Install the property-patterns skill in Claude Code
~/.claude/skills/property-patterns/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns ~/.claude/skills/property-patternsInstall the property-patterns skill in Claude Code (project)
.claude/skills/property-patterns/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns .claude/skills/property-patternsInstall the property-patterns skill in Codex
~/.agents/skills/property-patterns/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns ~/.agents/skills/property-patternsInstall the property-patterns skill in Gemini CLI
~/.gemini/skills/property-patterns/SKILL.md
$ npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns ~/.gemini/skills/property-patternsInstall the property-patterns skill in Claude.ai
npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns property-patterns && zip -r property-patterns.zip property-patterns # claude.ai → Customize → Skills → + → Create skill → Upload a skill → property-patterns.zip
Automated checks
Ran Nov 15, 2026Full report →Passed our automated checks. That is not a security audit: read the scripts before you let an agent run them. How checks work
- Source repository linked and reachabledotnet/skills/plugins/dotnet-msbuild/skills/property-patterns is public and reachablegithub.com/dotnet/skills/tree/HEAD/plugins/dotnet-msbuild/sk ↗Pass
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit 2026-11-11github.com/dotnet/skills/commits ↗Pass
- No download-and-run or credential theft in scriptsNo scripts in the skill folderPass
- No hidden instructions in SKILL.mdNo override phrases, safeguard switches, credential requests or hidden charactersgithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-msbuild/sk ↗Pass
- Pre-approved tools are scopedNo pre-approved tools; the agent asks before using onegithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-msbuild/sk ↗Pass
- SKILL.md follows the Agent Skills formatValid name and description; says when to use it; 168 linesgithub.com/dotnet/skills/blob/HEAD/plugins/dotnet-msbuild/sk ↗Pass
SKILL.md
View on GitHubMSBuild 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 in this skill
1property-patterns skill questions
How do I install the property-patterns skill in Claude Code?
Run npx degit dotnet/skills/plugins/dotnet-msbuild/skills/property-patterns ~/.claude/skills/property-patterns. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the property-patterns skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
Why are my DefineConstants being lost when I add new ones?
You are likely overwriting the property instead of appending to it. You must include the existing value using semicolon concatenation.
Where should I place TargetFramework-conditioned property groups?
Place them in .targets files or the project file itself, because TargetFramework is often empty in .props files during evaluation.