Items and Metadata

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.

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

Include

Adds items: one value, a ;-separated list, or wildcards (?, *, ** for any depth)

anywhere

Exclude

Filters this Include only (it does not remove items added by other elements)

with Include

Remove

Removes matching items already in the list (supports wildcards)

inside targets since .NET Framework 3.5; outside targets too since MSBuild 15.0

Update

Changes metadata of matching items already in the list, without adding new ones

outside targets only

KeepMetadata / RemoveMetadata

When copying items from another list, keep only / drop the named metadata

inside targets

KeepDuplicates

false skips an item whose identity and metadata already exist in the list

inside targets

MatchOnMetadata

With Remove="@(Other)", matches on metadata values instead of identity (MatchOnMetadataOptions: CaseSensitive, CaseInsensitive, PathLike)

with Remove

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

%(Identity)

the item as written: journals/2026/q1/jan.ledger.csv

%(FullPath)

the absolute path

%(RootDir), %(Directory)

the root (/, or C:\) and the directory without the root

%(RelativeDir)

journals/2026/q1/ — the path as written, minus the file name

%(RecursiveDir)

2026/q1/ — what ** matched (empty without a recursive wildcard)

%(Filename), %(Extension)

jan.ledger and .csv (only the last extension)

%(ModifiedTime), %(CreatedTime), %(AccessedTime)

file times

%(DefiningProjectFullPath), %(DefiningProjectDirectory), %(DefiningProjectName), %(DefiningProjectExtension)

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

Compile

Source files passed to the compiler. Link (display path), Visible, DependentUpon.

EmbeddedResource

Resources embedded in the assembly (.resx, or any file). LogicalName, WithCulture.

None

Files that are part of the project but not compiled: CopyToOutputDirectory (Always, PreserveNewest, IfDifferent), CopyToPublishDirectory, Pack, PackagePath.

Content

Files deployed with the app (web content); same copy metadata as None.

Reference

An assembly reference by name or HintPath (common in non-SDK projects).

ProjectReference

Another project to build first and reference. ReferenceOutputAssembly, PrivateAssets, SetTargetFramework, SkipGetTargetFrameworkProperties, Aliases.

PackageReference

A NuGet package (Version, PrivateAssets, IncludeAssets, ExcludeAssets, Aliases, GeneratePathProperty; see PackageReference).

InternalsVisibleTo

Generates [assembly: InternalsVisibleTo(...)]. Key for a strong-named friend.

AssemblyMetadata

Generates [assembly: AssemblyMetadata(key, value)]; the identity is the key, Value the value.

Using

Adds a global using (with Alias or Static="true"); Remove removes an implicit one.

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

  • Include of a file the SDK globs already include — NETSDK1022; use Update.

  • Expecting Exclude to remove items added elsewhere — it only filters its own Include; use Remove.

  • 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) on ledger.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 KeepMetadata also filters item-definition metadata — see the example above.