Claude Cursor GitHub Copilot Skill

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

LLM Mart · 0 points · 21 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download dotnet-skills-plugins_dotnet-msbuild_skills_property-patterns-98f8485.zip · 2 KB
Part of dotnet/skills — 119 skills

Install

skills CLI npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-msbuild/skills/property-patterns
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
Git 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 .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

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 Condition on defaults makes properties non-overridable. Add Condition="'$(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.

No comments yet.

Reviews (0)

No reviews yet.

Related