Conditions

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A Condition attribute makes an element take effect only when its expression is true. Conditions are how one project file serves several configurations, target frameworks, operating systems and CI systems, and they are also a common source of silent mistakes, because a condition that is false for the wrong reason produces no message at all. This page covers the syntax and its traps with MSBuild 18.9 (.NET SDK 10.0.402).

Operators

Operator Meaning

'a' == 'b', 'a' != 'b'

String equality, case-insensitive. Quotes are needed around anything that can be empty or contains spaces.

<, >, <=, >=

Numeric comparison of decimal, hexadecimal (0x…) or four-part dotted version operands; written < and > in XML.

Exists('path')

A file or folder exists (no wildcard expansion).

HasTrailingSlash('path')

The string ends with / or \.

!, And, Or, ( )

Logic. And binds tighter than Or; MSBuild warns when both appear without parentheses.

true, false

Boolean literals (Condition="false" works); !$(Prop) negates a property whose value is true or false.

$([MSBuild]::…) and other property functions

Anything that evaluates to a Boolean string: $([MSBuild]::IsOSPlatform('Windows')), $([MSBuild]::VersionGreaterThanOrEquals($(V), '17.8')), $(TargetFramework.StartsWith('net4'))

A demonstration

<Project>
  <PropertyGroup>
    <Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
    <OutputPath>out</OutputPath>
    <!-- String comparison is case-insensitive. -->
    <IsDebug Condition="'$(Configuration)' == 'DEBUG'">true</IsDebug>
    <!-- Exists and HasTrailingSlash. -->
    <HasJournals Condition="Exists('journals')">true</HasJournals>
    <OutputPath Condition="!HasTrailingSlash('$(OutputPath)')">$(OutputPath)/</OutputPath>
    <!-- Numeric and version comparisons. -->
    <NumericOk Condition="10 &gt; 9">true</NumericOk>
    <VersionOperator Condition="'1.1' &lt; '1.1.0'">1.1 &lt; 1.1.0 (System.Version)</VersionOperator>
    <VersionFunction Condition="$([MSBuild]::VersionLessThan('1.1', '1.1.0'))">1.1 &lt; 1.1.0 (VersionLessThan)</VersionFunction>
    <VersionFunction Condition="'$(VersionFunction)' == ''">1.1 == 1.1.0 (VersionLessThan is false)</VersionFunction>
    <!-- "true" checks: != 'false' is also true when the property is unset. -->
    <StrictCheck Condition="'$(Strict)' == 'true'">on</StrictCheck>
    <LooseCheck Condition="'$(Strict)' != 'false'">on</LooseCheck>
  </PropertyGroup>

  <Choose>
    <When Condition="'$(Configuration)' == 'Release'">
      <PropertyGroup><Optimize>true</Optimize></PropertyGroup>
    </When>
    <Otherwise>
      <PropertyGroup><Optimize>false</Optimize></PropertyGroup>
    </Otherwise>
  </Choose>

  <Target Name="Show">
    <Message Importance="high" Text="IsDebug=$(IsDebug) HasJournals=$(HasJournals) OutputPath=$(OutputPath) NumericOk=$(NumericOk)" />
    <Message Importance="high" Text="VersionOperator=$(VersionOperator)" />
    <Message Importance="high" Text="VersionFunction=$(VersionFunction)" />
    <Message Importance="high" Text="StrictCheck=[$(StrictCheck)] LooseCheck=[$(LooseCheck)] Optimize=$(Optimize)" />
  </Target>

  <Target Name="Precedence">
    <Message Importance="high" Condition="'$(A)' == '1' or '$(B)' == '1' and '$(C)' == '1'" Text="matched" />
  </Target>
</Project>
dotnet msbuild cond.proj -t:Show -nologo -v:m
dotnet msbuild cond.proj -t:Show -nologo -v:m -p:Configuration=Release | tail -1
  IsDebug=true HasJournals=true OutputPath=out/ NumericOk=true
  VersionOperator=1.1 < 1.1.0 (System.Version)
  VersionFunction=1.1 == 1.1.0 (VersionLessThan is false)
  StrictCheck=[] LooseCheck=[on] Optimize=false
  StrictCheck=[] LooseCheck=[on] Optimize=true

Three lines deserve a second look:

  • '$(Configuration)' == 'DEBUG' matched Debug: comparisons ignore case.

  • Versions. The relational operators parse four-part versions with System.Version, for which 1.1 is older than 1.1.0 — a missing part is -1, not 0. The $([MSBuild]::Version…) functions use SemVer-compatible rules, for which they are equal. Use the functions for anything that came from a version string.

  • Booleans. MSBuild has no Boolean type. '$(Strict)' != 'false' is true when Strict is unset, so LooseCheck turned on without anyone asking. Test for the value that enables a feature (== 'true').

Three errors and a warning

Precedence. Mixing and and or without parentheses is evaluated correctly (and first) but warned about, because older MSBuild versions got it wrong:

dotnet msbuild cond.proj -t:Precedence -nologo -v:m -p:A=1
~/cond/cond.proj(37,32): warning MSB4130: The condition "'$(A)' == '1' or '$(B)' == '1' and '$(C)' == '1'" may have been evaluated incorrectly in an earlier version of MSBuild. Please verify that the order of the AND and OR clauses is written as intended. To avoid this warning, add parentheses to make the evaluation order explicit.
  matched

Quoting. A value with a space must be quoted, or the parser stops at the second word:

<Ok Condition="$(Title) == Trial Balance">true</Ok>
~/cond/quote.proj(4,9): error MSB4092: An unexpected token "Balance" was found at character position 19 in condition "$(Title) == Trial Balance".

Quote both sides as a habit: '$(Title)' == 'Trial Balance'. An empty property then compares as '' instead of disappearing from the expression.

Items in the wrong place. Item lists (@) and metadata (%) are expanded in conditions only where items exist: in item groups and in targets. A property condition outside a target runs before any item:

<PropertyGroup>
  <HasAny Condition="'@(Journal)' != ''">true</HasAny>
</PropertyGroup>
~/cond/itemcond.proj(6,13): error MSB4099: A reference to an item list at position 2 is not allowed in this condition "'@(Journal)' != ''".

Move the check into a target, or into an item group (<HasJournals Include="x" Condition="'@(Journal)' != ''" />). Undefined properties, by contrast, never error: they expand to an empty string, so a misspelled property name in a condition makes it silently false (or, with !=, silently true).

Where conditions are allowed

Condition is accepted on Import, ImportGroup, Item (and ItemGroup, ItemDefinitionGroup, item metadata), Property and PropertyGroup, Target, task elements, Output, OnError, UsingTask and When. It is not accepted on Project, Choose, Otherwise or ProjectExtensions.

A target’s Condition is evaluated when the target is about to run. When it is false the target is skipped and its DependsOnTargets are not run, but targets that name it in BeforeTargets or AfterTargets still run, and a skipped target can still run later if another target requests it and the condition has become true (Targets and Build Order).

Choose, When and Otherwise

Choose selects the first When whose condition is true, or Otherwise. Each branch may contain PropertyGroup, ItemGroup and nested Choose elements. It reads better than repeating a condition on many properties, but it is evaluated like any other property and item group: properties in a Choose take part in the property pass, items in the item pass. It cannot contain targets.

Common mistakes

  • Unquoted operands — MSB4092, or an empty property vanishing from the expression.

  • != 'false' as a "true" test — true when the property is unset.

  • Version strings compared with < — 1.1 vs 1.1.0, 17.10 vs 17.9 as four-part versions; use the Version… functions.

  • @(…) in property conditions outside targets — MSB4099.

  • Conditions on $(TargetFramework) in Directory.Build.props — empty there except in multi-target inner builds (Repository-Wide Configuration).

  • Case-sensitive expectations — == ignores case.