MSBuild Item Management Patterns
Canonical patterns for working with item groups, from Microsoft.Common.CurrentVersion.targets.
Include / Remove / Update — Three Operations
| Operation | Purpose | When to use |
|---|---|---|
Include | Add new items to the group | Creating items with identity + metadata |
Remove | Remove items matching a pattern | Excluding files or clearing a group |
Update | Modify metadata on existing items | Adding/changing metadata without re-adding |
Include — Add Items
xml<ItemGroup> <Compile Include="Generated\*.cs"> <AutoGen>true</AutoGen> </Compile> </ItemGroup>
Remove — Subtract Items
xml<ItemGroup> <!-- Remove specific items --> <Reference Remove="$(AdditionalExplicitAssemblyReferences)" /> <!-- Set subtraction: prior minus current --> <_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanCurrentFileWrites)" /> <!-- Clear an entire group --> <_Temporary Remove="@(_Temporary)" /> </ItemGroup>
Update — Modify Existing Items
xml<ItemGroup> <EmbeddedResource Update="@(EmbeddedResource)" Condition="'%(NuGetPackageId)' == 'Microsoft.CodeAnalysis.Collections'"> <GenerateSource>true</GenerateSource> <ClassName>Microsoft.CodeAnalysis.Collections.SR</ClassName> </EmbeddedResource> </ItemGroup>
Update does not add items — it only modifies items already in the group.
Item Batching — %(Metadata)
When %(Metadata) appears in target attributes or task parameters, MSBuild batches execution per unique metadata value.
Target-level batching (Outputs)
xml<Target Name="GenerateSatelliteAssemblies" Inputs="$(MSBuildAllProjects);@(_SatelliteAssemblyResourceInputs)" Outputs="$(IntermediateOutputPath)%(Culture)\$(TargetName).resources.dll"> <!-- Runs once per unique Culture value --> </Target>
Task-level batching
xml<Copy SourceFiles="@(_SourceItems)" DestinationFiles="@(_SourceItems->'$(OutDir)%(TargetPath)')"> </Copy>
Per-item filtering with Condition
xml<ItemGroup> <_ResxOutput Include="@(EmbeddedResource->'%(OutputResource)')" Condition="'%(EmbeddedResource.WithCulture)' == 'false'" /> </ItemGroup>
Batching rules
%(Metadata)inConditionorOutputs→ target batches per unique value.%(Metadata)in task parameters → task batches per unique value.- Do not mix
%()from different item groups in the same expression — this causes a cross-product (see Common Pitfalls).
Item Transforms — @(Item->'expression')
Transforms create new item lists by applying an expression to each item:
xml<!-- Transform file paths to destinations --> <Copy SourceFiles="@(IntermediateAssembly)" DestinationFiles="@(IntermediateAssembly->'$(OutDir)%(Filename)%(Extension)')"/> <!-- Transform with separator for display --> <Message Text="Files: @(Compile->'%(Filename)', ', ')" />
Exclude Pattern — Set Subtraction on Include
xml<ItemGroup> <Compile Include="**\*.cs" Exclude="Generated\**;Tests\**" /> </ItemGroup>
Exclude only works on Include — it cannot be used with Update or Remove.
Conditional Item Inclusion
xml<!-- Condition on ItemGroup — all or nothing --> <ItemGroup Condition="'$(NetCoreBuild)' == 'true'"> <PackageReference Include="System.IO.Pipelines" /> </ItemGroup> <!-- Condition on individual items --> <ItemGroup> <PackageReference Include="System.IO.Pipelines" Condition="'$(NetCoreBuild)' == 'true'" /> </ItemGroup>
PrivateAssets on Tool/Analyzer Packages
xml<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" PrivateAssets="all" /> <PackageReference Include="StyleCop.Analyzers" PrivateAssets="all" /> </ItemGroup>
Common Pitfalls
Cross-product batching
Referencing %(Metadata) from two different item groups creates O(N×M) executions:
xml<!-- BAD: Cross-product of @(Source) × @(Config) --> <Exec Command="process %(Source.Identity) with %(Config.Identity)" /> <!-- GOOD: Reference one group via batching, the other via property --> <Exec Command="process %(Source.Identity) with $(ConfigFile)" />
Generated files in source tree
Write to $(IntermediateOutputPath) (obj/), not the source directory. Source-tree generation pollutes version control and can cause duplicate compilation via globs.
Missing FileWrites
Every file created during a target must be added to @(FileWrites) for dotnet clean support.

