New to Claude Skills? Learn how to install them →

Mdotnet on GitHub

MSBuild Property Patterns

Free

Streamline your MSBuild property definitions and management.

by dotnet5.1k stars on dotnet/skills
1 views
Updated Aug 10, 2026
Get this skill

Free · Opens the source repo

What MSBuild Property Patterns does

The MSBuild Property Patterns skill provides a comprehensive set of guidelines and patterns for defining and manipulating properties in MSBuild projects. It focuses on best practices for conditional defaults, property composition, path normalization, and target framework detection, which are essential for creating robust and maintainable build scripts. This skill is particularly useful for developers working with .props and .csproj files who need to diagnose and fix property definition issues or shared-property anti-patterns.

By following the outlined patterns, users can ensure that properties are set only when necessary, allowing for easier overrides and preventing unwanted behavior in build processes. The skill emphasizes the importance of quoting conditions, using nested conditional groups, and maintaining proper evaluation order to avoid common pitfalls. For instance, it warns against hardcoded paths that can break cross-platform compatibility and provides strategies for setting overridable defaults effectively.

This skill is ideal for developers who want to enhance their MSBuild scripts' reliability and portability. It serves as a reference for best practices, helping to avoid mistakes that can lead to build failures or unexpected behavior. The patterns also assist in managing complex build scenarios where multiple properties interact, ensuring that the last write wins and that properties are evaluated correctly.

Whether you are working on a new project or maintaining an existing one, the MSBuild Property Patterns skill can help you streamline your property definitions and improve your build process, making it easier to manage configurations across different environments.

When to use it

Use this skill when you need to define or troubleshoot properties in MSBuild projects, particularly in .props and .csproj files.

When not to use it

This skill is not suitable for general MSBuild anti-patterns or for managing item operations and target structures; other skills are better suited for those tasks.

What you can build with it

Setting Up Conditional Defaults

Use the skill to define properties that should only be set if they are not already defined, allowing for flexible project configurations.

Normalizing Paths for Cross-Platform Builds

Implement path normalization patterns to ensure that your builds work seamlessly across different operating systems.

Diagnosing Property Definition Issues

Leverage the guidelines to identify and fix common property definition problems in your MSBuild scripts.

How to install MSBuild Property Patterns

View source

1. Install with the skills CLI

npx skills add dotnet/skills/property-patterns --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by dotnet

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 .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:

<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

FunctionPurpose
$([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 Condition on defaults makes properties non-overridable. Add Condition="'$(Prop)' == ''" for values meant to be defaults.

Frequently asked questions about MSBuild Property Patterns

Similar skills