Skip to main content
skill CX Codex

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.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dotnet/skills --skill property-patterns -a claude-code
Open on GitHub

What 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 DefineConstants without losing earlier constants
  • replace a hardcoded absolute path in .props
  • move TFM-conditioned groups out of .props into .targets

Example prompts for the agent

  • Configuration is always Debug in this project. Check the conditions in Directory.Build.props and fix the defaults.
  • My TargetFramework condition in .props never fires. Figure out why and move the group somewhere it works.
  • Go through the properties in our shared files and show me where DefineConstants gets overwritten.

How to install property-patterns

1Skills CLI (Vercel)

npx skills add dotnet/skills --skill property-patterns -a claude-code

2Manually: put the files in place

~/.claude/skills/property-patterns/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/property-patterns/

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

Similar skills

Everything for DevOps and infrastructure →

From the same set101

plan-ui-change Splits a complex Blazor page into focused components before the agent writes any code. Useful once a screen has three or more visual sections. CX Codex +1 3,000installs collect-user-input A skill that walks the agent through Blazor forms, input components and validation. Useful when you add a form, a search box or file upload to a page. CX Codex +1 3,000installs coordinate-components Helps AI agents share state across Blazor components without parent-child parameter chains. Covers cascading parameters, scoped services, and render mode boundaries. CX Codex +1 3,000installs create-blazor-project The skill walks the agent through picking a Blazor render mode and runs dotnet new blazor with the right flags. CX Codex +1 3,000installs author-component Creates and reviews Blazor components without JavaScript interop. Useful when setting up parameters, RenderFragment slots, and proper async lifecycles. CX Codex +1 3,100installs test-analysis-extensions A reference catalog of per-language lookup tables for test analysis: markers, assertions, skips, sleep patterns. The agent reads the file matching your stack. CX Codex +1 3,100installs find-untested-sources Finds source files lacking corresponding tests using static AST parsing without project builds. Useful for coding agents before generating new test suites. CX Codex +1 3,100installs fetch-and-send-data Guides API calls and asynchronous data loading in Blazor components. Use it when configuring HttpClient, loading states, or error handling. CX Codex +1 3,100installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor