Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ The folder contains collection of docs and references for MSBuild, detailed info

### Problems?

* [Common MSBuild gotchas and limitations](wiki/Common-MSBuild-Gotchas.md)
* [Rebuilding when nothing changed](wiki/Rebuilding-when-nothing-changed.md)
* [Controling References Behavior](wiki/Controlling-Dependencies-Behavior.md)
* [Something's wrong in my build](wiki/Something's-wrong-in-my-build.md)
Expand Down
124 changes: 124 additions & 0 deletions documentation/wiki/Common-MSBuild-Gotchas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Common MSBuild Gotchas and Limitations

This document describes common pitfalls, limitations, and unexpected behaviors in MSBuild that developers frequently encounter.

## Item Metadata in Conditions Outside of Targets

### The Issue

When using `ItemGroup` elements with conditions that reference item metadata (using the `%(ItemType.MetadataName)` syntax), these conditions only work inside `Target` elements, not at the project level (outside of targets).

### Error Message

If you try to use item metadata in a condition outside of a target, you'll get an error like:

```
error MSB4191: The reference to custom metadata "X" at position 1 is not allowed in this condition "'%(Content.X)' == 'abc'".
```

### Example

This **does NOT work** at the project level:

```xml
<Project>
<ItemGroup>
<Content Include="file1.txt">
<X>abc</X>
</Content>
<Content Include="file2.txt">
<X>def</X>
</Content>
</ItemGroup>

<!-- This will FAIL with MSB4191 error -->
<ItemGroup>
<FilteredContent Include="@(Content)" Condition="'%(Content.X)' == 'abc'" />
</ItemGroup>
</Project>
```

This **DOES work** inside a target:

```xml
<Project>
<ItemGroup>
<Content Include="file1.txt">
<X>abc</X>
</Content>
<Content Include="file2.txt">
<X>def</X>
</Content>
</ItemGroup>

<Target Name="FilterItems">
<!-- This works inside a target -->
<ItemGroup>
<FilteredContent Include="@(Content)" Condition="'%(Content.X)' == 'abc'" />
</ItemGroup>
<Message Text="FilteredContent: @(FilteredContent)" />
</Target>
</Project>
```

### Why This Happens

The `%(ItemType.MetadataName)` syntax implies **batching** - evaluating the condition once for each distinct value of the metadata. Batching only happens inside `Target` elements during target execution, not during project evaluation (which processes everything outside of targets).

Outside of targets, MSBuild evaluates each element as a single entity. The batching infrastructure required to split items into buckets based on metadata values is only available during target execution.

### Workarounds

#### 1. Move the Item Filtering to a Target

The simplest solution is to move your item filtering logic into a target:

```xml
<Target Name="FilterItems" BeforeTargets="Build">
<ItemGroup>
<FilteredContent Include="@(Content)" Condition="'%(Content.X)' == 'abc'" />
</ItemGroup>
</Target>
```

#### 2. Use `WithMetadataValue` Item Function

For project-level filtering, use the `->WithMetadataValue()` item function instead:

```xml
<ItemGroup>
<!-- This works at project level -->
<FilteredContent Include="@(Content->WithMetadataValue('X', 'abc'))" />
</ItemGroup>
```

The `WithMetadataValue` function is specifically designed for filtering items by metadata at the project level without requiring batching.

#### 3. Use Other Item Functions

MSBuild provides several [item functions](https://learn.microsoft.com/visualstudio/msbuild/item-functions) that can be used at the project level:

```xml
<ItemGroup>
<!-- Filter using metadata value -->
<FilteredContent Include="@(Content->WithMetadataValue('X', 'abc'))" />

<!-- Check if metadata exists -->
<ItemsWithMetadata Include="@(Content->HasMetadata('X'))" />

<!-- Transform based on metadata -->
<TransformedContent Include="@(Content->'%(X)')" />
</ItemGroup>
```

### Related Information

- [MSBuild Batching](https://learn.microsoft.com/visualstudio/msbuild/msbuild-batching)
- [Item Functions](https://learn.microsoft.com/visualstudio/msbuild/item-functions)
- [MSBuild Items](https://learn.microsoft.com/visualstudio/msbuild/msbuild-items)

### Further Reading

For more context on why this limitation exists, see:
- [MSBuild Architecture Overview](../Contributions/MSBuild-overview.md) - explains the difference between evaluation and execution phases
- Issue [#3520](https://github.com/dotnet/msbuild/issues/3520) - original issue tracking this limitation
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue number referenced here (#3520) does not match the issue number mentioned in the PR description (#3479). The link should point to the correct issue that this documentation is addressing.

Suggested change
- Issue [#3520](https://github.com/dotnet/msbuild/issues/3520) - original issue tracking this limitation
- Issue [#3479](https://github.com/dotnet/msbuild/issues/3479) - original issue tracking this limitation

Copilot uses AI. Check for mistakes.