Property Functions, Item Functions and Transforms
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its 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. |
MSBuild has no statements, but it can compute: property functions call .NET methods and MSBuild helpers inside
$(...), item functions filter and reshape item lists inside @(...), and transforms map each item of a list
to a new string. They are what lets one Directory.Build.props find the repository root, one target turn a list of
inputs into a list of outputs, and a condition compare versions properly. This page runs most of them in a single
Ledgerline project with MSBuild 18.9 (.NET SDK 10.0.402).
The three syntaxes
| Form | Example |
|---|---|
String instance method on a property |
|
Static method or property of an allowed .NET class |
|
MSBuild helper |
|
Arguments are strings unless they look like numbers or booleans; quote string arguments with single quotes, and
quote property references inside arguments ('$(TargetFramework)') so an empty value is still an argument. Calls nest,
and the result of one can be the target of another ($([System.String]::Copy($(X)).ToUpperInvariant())).
A tour in one project
The project sits in src/Ledgerline of a small repository with a Directory.Build.props at its root, two
*.ledger.csv journals and two .cs files:
<Project>
<PropertyGroup>
<Period> q1 2026 </Period>
<TargetFramework>net8.0</TargetFramework>
<!-- String instance methods. -->
<PeriodTrimmed>$(Period.Trim())</PeriodTrimmed>
<Year>$(PeriodTrimmed.Substring(3, 4))</Year>
<!-- Static .NET methods (from the allowed list). -->
<JournalCount>$([System.IO.Directory]::GetFiles('journals', '*.ledger.csv').Length)</JournalCount>
<Upper>$([System.String]::Copy($(PeriodTrimmed)).ToUpperInvariant())</Upper>
<!-- [MSBuild]:: helpers. -->
<RepoRoot>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'Directory.Build.props'))</RepoRoot>
<PropsPath>$([MSBuild]::GetPathOfFileAbove('Directory.Build.props'))</PropsPath>
<RelativeFromRoot>$([MSBuild]::MakeRelative($(RepoRoot), $(MSBuildProjectDirectory)))</RelativeFromRoot>
<Region>$([MSBuild]::ValueOrDefault('$(LEDGERLINE_REGION)', 'EU'))</Region>
<Hash>$([MSBuild]::StableStringHash('Ledgerline'))</Hash>
<Tfi>$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)'))</Tfi>
<Tfv>$([MSBuild]::GetTargetFrameworkVersion('$(TargetFramework)'))</Tfv>
<IsNet8OrLater>$([MSBuild]::IsTargetFrameworkCompatible('net10.0', '$(TargetFramework)'))</IsNet8OrLater>
<NewEnough>$([MSBuild]::VersionGreaterThanOrEquals('$(MSBuildVersion)', '17.8'))</NewEnough>
<OnLinux>$([MSBuild]::IsOSPlatform('Linux'))</OnLinux>
<Sum>$([MSBuild]::Add(40, 2))</Sum>
<!-- Nested calls. -->
<ShortName>$([System.IO.Path]::GetFileNameWithoutExtension('$([System.IO.Path]::GetFileName('$(MSBuildProjectFullPath)'))'))</ShortName>
</PropertyGroup>
<ItemGroup>
<Compile Include="*.cs" />
<Journal Include="journals/*.ledger.csv" Month="%(Filename)" />
<Journal Update="journals/jan.ledger.csv" Audited="true" />
</ItemGroup>
<Target Name="Show">
<Message Importance="high" Text="PeriodTrimmed=[$(PeriodTrimmed)] Year=$(Year) Upper=$(Upper) JournalCount=$(JournalCount)" />
<Message Importance="high" Text="RepoRoot=$(RepoRoot)" />
<Message Importance="high" Text="PropsPath=$(PropsPath) RelativeFromRoot=$(RelativeFromRoot)" />
<Message Importance="high" Text="Region=$(Region) Hash=$(Hash) Sum=$(Sum) ShortName=$(ShortName)" />
<Message Importance="high" Text="Tfi=$(Tfi) Tfv=$(Tfv) net10.0 can use net8.0 assets: $(IsNet8OrLater) MSBuild>=17.8: $(NewEnough) Linux: $(OnLinux)" />
<!-- Transforms. -->
<Message Importance="high" Text="Generated: @(Compile->'obj/%(Filename).g.cs')" />
<Message Importance="high" Text="Custom separator: @(Journal->'%(Filename)', ' | ')" />
<!-- Item functions. -->
<Message Importance="high" Text="Count=@(Journal->Count()) Audited=@(Journal->WithMetadataValue('Audited', 'true')) AnyAudited=@(Journal->AnyHaveMetadataValue('Audited', 'true'))" />
<Message Importance="high" Text="Dirs=@(Journal->DirectoryName()->Distinct()) Reversed=@(Compile->Reverse()) Upper=@(Compile->ToUpperInvariant())" />
<Message Importance="high" Text="Metadata=@(Journal->Metadata('Month')) HasMetadata=@(Journal->HasMetadata('Audited'))" />
</Target>
</Project>
dotnet msbuild funcs.proj -t:Show -nologo -v:m
PeriodTrimmed=[q1 2026] Year=2026 Upper=Q1 2026 JournalCount=2
RepoRoot=~/funcs/repo
PropsPath=~/funcs/repo/Directory.Build.props RelativeFromRoot=src/Ledgerline
Region=EU Hash=1729098504 Sum=42 ShortName=funcs
Tfi=.NETCoreApp Tfv=8.0 net10.0 can use net8.0 assets: True MSBuild>=17.8: True Linux: True
Generated: obj/Ledger.g.cs;obj/Reader.g.cs
Custom separator: feb.ledger | jan.ledger
Count=2 Audited=journals/jan.ledger.csv AnyAudited=true
Dirs=~/funcs/repo/src/Ledgerline/journals Reversed=Reader.cs;Ledger.cs Upper=LEDGER.CS;READER.CS
Metadata=feb.ledger;jan.ledger HasMetadata=journals/jan.ledger.csv
-getProperty shows the same values without a target; setting the environment variable changes the
ValueOrDefault result:
LEDGERLINE_REGION=US dotnet msbuild funcs.proj -getProperty:Region
US
The [MSBuild]:: helpers
| Function | Use |
|---|---|
|
The directory of the nearest |
|
The full path of the nearest |
|
|
|
Combine and canonicalise paths with the right separators ( |
|
|
|
A hash that is the same on every machine and run (unlike |
|
Parse TFMs ( |
|
Compare versions numerically ( |
|
Operating-system checks for conditions. |
|
Arithmetic. |
|
Others listed on the reference page. |
Which .NET methods are allowed
Static property functions are restricted to a list of classes — System.String, System.IO.Path, System.Math,
System.Version, System.Guid, System.DateTime, System.Text.RegularExpressions.Regex,
System.Runtime.InteropServices.RuntimeInformation, the integer types and a few others — plus named read-only members
of System.Environment, System.IO.Directory and System.IO.File (Exists, ReadAllText, GetFiles, …). On the
.NET MSBuild every static method of System.OperatingSystem is available too; msbuild.exe allows only
IsOSPlatform and IsOSPlatformVersionAtLeast. Since MSBuild 17.14, an out _ argument discards an out parameter
($([System.Int32]::TryParse("123", out _))). Anything else is refused, so evaluation cannot write files:
<Project>
<PropertyGroup>
<Oops>$([System.IO.File]::WriteAllText('x.txt', 'hi'))</Oops>
</PropertyGroup>
<Target Name="T" />
</Project>
~/funcs/bad.proj(3,5): error MSB4185: The function "WriteAllText" on type "System.IO.File" is not available for execution as an MSBuild property function.
A function that throws reports MSB4184 with the .NET exception message — here Substring(3, 4) on a one-character
Period:
dotnet msbuild funcs.proj -getProperty:Year -p:Period=x
~/funcs/repo/src/Ledgerline/funcs.proj(7,5): error MSB4184: The expression ""x".Substring(3, 4)" cannot be evaluated. startIndex cannot be larger than length of string. (Parameter 'startIndex')
Source: MSBuild: Static property functions.
Item functions
Item functions run on a list and return a list (or a value):
| Function | Result |
|---|---|
|
the number of items |
|
duplicates removed (case-insensitive / case-sensitive) |
|
the list reversed |
|
path operations on each item |
|
filter on a metadata value |
|
|
|
items that have |
|
any |
Functions chain: @(Journal->DirectoryName()->Distinct()) above gave the one folder the journals share.
Transforms
A transform maps every item through a template that may contain metadata references:
@(Compile->'obj/%(Filename).g.cs') turned Ledger.cs;Reader.cs into obj/Ledger.g.cs;obj/Reader.g.cs. Any
metadata can be used, several may be combined ('%(RecursiveDir)%(Filename).g%(Extension)'), and a second argument
replaces the default ; separator (@(Journal->'%(Filename)', ' | ')). Because a transform preserves the order and
the one-to-one relationship between inputs and outputs, a target whose Outputs is a transform of its Inputs can
rebuild only the outputs whose input changed
(Incremental Builds and Batching).
Common mistakes
-
Unquoted property references as arguments — an empty property removes the argument and the call fails or picks another overload; write
'$(X)'. -
Comparing versions as strings in conditions — use
VersionGreaterThanOrEquals. -
Using
GetHashCode()for file names — not stable across processes; useStableStringHash. -
Calling a method that is not on the allowed list —
MSB4185; move the logic into a task. -
@(X->'...')in a property outside a target — stored as text and expanded later (Evaluation, then execution).