Items and Metadata
|
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. |
Items are MSBuild’s lists. An item has a type (Compile, Journal), an identity (usually a file path, but any
string) and metadata (name—value pairs attached to that one item). @(Compile) expands to the list joined with ;,
%(Compile.Filename) reads metadata and makes a task run once per distinct value. Source files, references, packages,
content to copy and files to pack are all items, so knowing how to add, filter and annotate them is most of what
customising a .NET build means. Examples use Ledgerline's journal files and MSBuild 18.9 (.NET SDK 10.0.402).
Creating and changing items
| Attribute | Effect | Where |
|---|---|---|
|
Adds items: one value, a |
anywhere |
|
Filters this |
with |
|
Removes matching items already in the list (supports wildcards) |
inside targets since .NET Framework 3.5; outside targets too since MSBuild 15.0 |
|
Changes metadata of matching items already in the list, without adding new ones |
outside targets only |
|
When copying items from another list, keep only / drop the named metadata |
inside targets |
|
|
inside targets |
|
With |
with |
A project over a small journal folder (two files in Q1, one in Q2, one archived, plus a notes.txt):
<Project>
<ItemDefinitionGroup>
<Journal>
<Currency>EUR</Currency>
<Audited>false</Audited>
</Journal>
</ItemDefinitionGroup>
<ItemGroup>
<!-- ** matches any depth; Exclude filters this Include only. -->
<Journal Include="journals/**/*.ledger.csv" Exclude="journals/archive/**" />
<!-- Update changes metadata of items already in the list (outside targets too). -->
<Journal Update="journals/2026/q1/*.ledger.csv" Audited="true" />
<!-- Items need not be files. -->
<Account Include="1000-Cash;3000-Equity;1000-Cash" />
</ItemGroup>
<Target Name="Show">
<Message Importance="high" Text="Journals: @(Journal)" />
<Message Importance="high" Text=" %(Journal.Filename)%(Journal.Extension) RecursiveDir=%(Journal.RecursiveDir) Audited=%(Journal.Audited) Currency=%(Journal.Currency)" />
<Message Importance="high" Text="Accounts (with the duplicate): @(Account)" />
<ItemGroup>
<!-- Inside a target: KeepDuplicates and Remove. -->
<UniqueAccount Include="@(Account)" KeepDuplicates="false" />
<Journal Remove="journals/2026/q2/**" />
<!-- KeepMetadata copies only the named metadata. -->
<AuditList Include="@(Journal)" KeepMetadata="Audited" />
</ItemGroup>
<Message Importance="high" Text="Unique accounts: @(UniqueAccount)" />
<Message Importance="high" Text="After Remove: @(Journal)" />
<Message Importance="high" Text="AuditList keeps Audited=[%(AuditList.Audited)] Currency=[%(AuditList.Currency)]" />
</Target>
</Project>
dotnet msbuild items.proj -t:Show -nologo -v:m
Journals: journals/2026/q1/feb.ledger.csv;journals/2026/q1/jan.ledger.csv;journals/2026/q2/apr.ledger.csv
feb.ledger.csv RecursiveDir=2026/q1/ Audited=true Currency=EUR
jan.ledger.csv RecursiveDir=2026/q1/ Audited=true Currency=EUR
apr.ledger.csv RecursiveDir=2026/q2/ Audited=false Currency=EUR
Accounts (with the duplicate): 1000-Cash;3000-Equity;1000-Cash
Unique accounts: 1000-Cash;3000-Equity
After Remove: journals/2026/q1/feb.ledger.csv;journals/2026/q1/jan.ledger.csv
AuditList keeps Audited=[true] Currency=[EUR]
Observations: wildcard matches come back sorted; notes.txt and the archive were filtered by the glob and Exclude;
%(RecursiveDir) is the part of the path matched by **, which is how a copy target preserves folder structure; and a
plain list of strings (Account) is as much an item as a file. The last line shows a subtlety: Currency survived
KeepMetadata="Audited", because it comes from the item definition of Journal rather than from the items
themselves. With the same metadata set directly on the item, KeepMetadata drops it as documented:
<Journal Include="jan.ledger.csv" Currency="EUR" Audited="true" Owner="ana" />
…
<Kept Include="@(Journal)" KeepMetadata="Audited" />
<Removed Include="@(Journal)" RemoveMetadata="Owner" />
Kept: Audited=[true] Currency=[] Owner=[]
Removed: Audited=[true] Currency=[EUR] Owner=[]
Metadata
Metadata is written as attributes (<Journal Include="x" Audited="true" />) or child elements, and read as
%(Journal.Audited) (or %(Audited) when the item type is clear from context). Referencing %(…) in a task parameter
or a target attribute batches the work by distinct values — the Message above ran three times. Inside a transform it
is evaluated per item instead: @(Journal->'%(Filename)')
(Transforms).
Well-known metadata
Every item that is a file path gets these read-only metadata automatically:
| Metadata | For journals/2026/q1/jan.ledger.csv |
|---|---|
|
the item as written: |
|
the absolute path |
|
the root ( |
|
|
|
|
|
|
|
file times |
|
the file that defined the item — useful to resolve relative paths in imported files |
dotnet msbuild -getItem:Journal lists all of them (the JSON for one Ledgerline item is shown on
How a .NET Build Works).
Item definitions
ItemDefinitionGroup gives every item of a type default metadata, evaluated in its own pass after properties and before
items. Metadata set on an item overrides the default (Audited="true" above); several definitions for one type are
additive, and a definition can reference earlier metadata of the same type.
The common items
These are the item types the common targets and the SDK act on; most come from the SDK’s default globs (see SDK-Style Project Files).
| Item | Meaning and common metadata |
|---|---|
|
Source files passed to the compiler. |
|
Resources embedded in the assembly ( |
|
Files that are part of the project but not compiled: |
|
Files deployed with the app (web content); same copy metadata as |
|
An assembly reference by name or |
|
Another project to build first and reference. |
|
A NuGet package ( |
|
Generates |
|
Generates |
|
Adds a |
Adding three of them to the Ledgerline library project:
<ItemGroup>
<InternalsVisibleTo Include="Ledgerline.Tests" />
<AssemblyMetadata Include="LedgerFormat" Value="csv-v1" />
<Using Include="System.Globalization" />
</ItemGroup>
and building net10.0 shows the generated attributes (only the relevant lines) and the new global using:
dotnet build -f net10.0
grep -E "InternalsVisibleTo|AssemblyMetadata" obj/Debug/net10.0/Ledgerline.AssemblyInfo.cs
cat obj/Debug/net10.0/Ledgerline.GlobalUsings.g.cs
[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://example.com/ledgerline/ledgerline")]
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Ledgerline.Tests")]
[assembly: System.Reflection.AssemblyMetadata("LedgerFormat", "csv-v1")]
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.Globalization;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
The RepositoryUrl metadata attribute comes from the RepositoryUrl property in Directory.Build.props (Source
Link adds it).
Cross-tool notes
Visual Studio’s Solution Explorer shows Compile, None, Content and EmbeddedResource items and edits their
metadata through the Properties window (Build Action, Copy to Output Directory); with SDK-style projects it writes
Update items rather than listing files. dotnet has no item-editing command except for references and packages
(dotnet reference add, dotnet package add).
Common mistakes
-
Includeof a file the SDK globs already include —NETSDK1022; useUpdate. -
Expecting
Excludeto remove items added elsewhere — it only filters its ownInclude; useRemove. -
Wildcards in an item outside a target that match files generated during the build — the glob ran during evaluation, before the files existed; include generated files inside the target that creates them.
-
%(Filename)onledger.csv.bak— the extension is only.bak. -
A
%(…)reference in a message or task parameter that unexpectedly runs the task many times — that is batching. -
Assuming
KeepMetadataalso filters item-definition metadata — see the example above.