Skip to content

Expose item glob patterns via MSBuildProvideItemGlobs#14389

Open
drewnoakes wants to merge 1 commit into
dotnet:mainfrom
drewnoakes:dev/drnoakes/expose-item-globs
Open

Expose item glob patterns via MSBuildProvideItemGlobs#14389
drewnoakes wants to merge 1 commit into
dotnet:mainfrom
drewnoakes:dev/drnoakes/expose-item-globs

Conversation

@drewnoakes

@drewnoakes drewnoakes commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Adds an opt-in mechanism that surfaces a project's unevaluated include/exclude/remove glob patterns to targets and tasks during an ordinary command-line build. When the MSBuildProvideItemGlobs property is set to a semicolon-separated list of item types (for example Compile;Content), the engine synthesizes MSBuildItemGlob items during evaluation — one per include element that contributes globs — carrying the patterns in item metadata.

This follows the same pattern as #13681, which exposed a project's import tree to targets and tasks as MSBuildImportedProject items. This change does the equivalent for a project's item globs.

Motivation

External tooling frequently needs to answer a simple question cheaply and often: "a file just appeared on disk — does it belong to this project, and to which item type?" A file watcher, for instance, wants to know whether a newly created .cs file is picked up by the project's compile globs without re-running a full evaluation on every file-system event.

Today the only reliable ways to obtain a project's wildcard patterns are to re-evaluate the project or to drive MSBuild through its object model and call Project.GetAllGlobs(). Both require hosting the engine in-process, as Visual Studio's project system and the C#/Roslyn language server do. There is no way to extract the same information from a plain dotnet build or msbuild invocation, where a tool has only the build output to work with.

This change closes that gap using regular MSBuild items. Because they are ordinary items, they flow to every target and task automatically, serialize to out-of-proc worker nodes for free, and require no new public API surface.

Usage

Set the property to the item types of interest, then read the synthesized items from a target or task:

<PropertyGroup>
  <MSBuildProvideItemGlobs>Compile;Content</MSBuildProvideItemGlobs>
</PropertyGroup>

<Target Name="ShowGlobs">
  <Message Importance="high"
           Text="%(MSBuildItemGlob.Identity): include=%(MSBuildItemGlob.Include); exclude=%(MSBuildItemGlob.Exclude); remove=%(MSBuildItemGlob.Remove)" />
</Target>

The property is intended primarily for tooling that consumes the build output, so it is typically supplied as a global property rather than authored into the project. It can be set in the project file, in a Directory.Build.props, through ProjectCollection.GlobalProperties, or on the command line.

Each synthesized MSBuildItemGlob item represents a single include element and carries:

  • Identity — the item type (for example Compile). Multiple include elements of the same type each produce their own item, so identities repeat and appear in document order.
  • Include — the include glob pattern(s) of the element, with property references expanded and wildcards preserved.
  • Exclude — the exclude pattern(s) present on the element (literals and globs).
  • Remove — the remove pattern(s) that apply to the element (literals and globs).

How it works

The patterns are derived by exactly the same logic that backs Project.GetAllGlobs(). That logic has been lifted out of Project into a new shared, generic GlobResultBuilder, so the public API and the synthesized items are guaranteed to produce identical results and cannot drift apart.

Synthesis happens in the evaluator at the end of the items pass, in Evaluator.SynthesizeItemGlobItems. Doing it during evaluation — rather than during ProjectInstance construction — ensures the items appear identically on both Project and ProjectInstance. Doing it at the end of the pass, after all items have been evaluated, ensures that item references (@(...)) inside exclude and remove expressions resolve to their final values, matching GetAllGlobs.

Design notes

  • Zero cost when unused. If MSBuildProvideItemGlobs is not set, the feature does no work and allocates nothing.
  • Wildcards are never expanded. The patterns live in item metadata, which is never globbed against the file system, so **/*.cs is preserved verbatim rather than being expanded into a file list. Metadata values are standard MSBuild-escaped, semicolon-separated lists, so each pattern round-trips to exactly the string GetAllGlobs reports — including patterns containing %, and (recovered from the escaped value, as with any MSBuild item list) even the rare pattern containing a literal ;.
  • Precedence is preserved. Items are emitted in document order, and each include record carries only the removes that apply to it (those that appear after it in the file). Replaying the records therefore reproduces the authored include/exclude/remove precedence, including "included, then removed, then re-included" sequences where ordering is significant.
  • Literal includes are omitted; literal excludes and removes are kept. A change to a file that is included by an exact path is always accompanied by a project-file edit, so a watcher re-evaluates anyway. Excludes and removes retain both their literal and glob fragments, which a consumer needs in order to decide membership.

Tests

ItemGlobs_Tests covers opt-out (no items synthesized), opt-in, include/exclude/remove capture, retention of literal excludes and removes, omission of literal includes, item-type filtering, multiple item types, property expansion with wildcard preservation, document order, remove attribution and re-inclusion ordering, conditioned-out elements, the global-property path, Project and ProjectInstance parity, parity with Project.GetAllGlobs(), escaping round-trips for patterns containing %, lossless recovery of a pattern that itself contains a literal ;, and end-to-end availability to a building target.

After merge

File an issue to add a BuildCheck analyzer to prevent use of these items. BuildCheck supports property checks, but not yet item checks. So there's some infrastructural work required before this can be added, which is why it's not part of this PR.

Copilot AI review requested due to automatic review settings July 16, 2026 04:36
@drewnoakes
drewnoakes temporarily deployed to copilot-pat-pool July 16, 2026 04:36 — with GitHub Actions Inactive
@drewnoakes
drewnoakes temporarily deployed to copilot-pat-pool July 16, 2026 04:36 — with GitHub Actions Inactive
@drewnoakes
drewnoakes temporarily deployed to copilot-pat-pool July 16, 2026 04:37 — with GitHub Actions Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds an opt-in evaluation feature that exposes a project’s include/exclude/remove glob patterns as synthesized MSBuildItemGlob items when MSBuildProvideItemGlobs is set, and refactors the existing Project.GetAllGlobs() implementation into a shared GlobResultBuilder to keep both paths in sync.

Changes:

  • Introduces MSBuildProvideItemGlobs and synthesized MSBuildItemGlob items (schema + docs).
  • Refactors glob derivation logic into Evaluation/GlobResultBuilder.cs and uses it from both evaluator synthesis and Project.GetAllGlobs().
  • Adds unit tests validating opt-in behavior, ordering, escaping, and target visibility.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd Adds XSD entries for MSBuildItemGlob item and MSBuildProvideItemGlobs property.
src/Framework/Constants.cs Adds constant names for the new opt-in property, item type, and metadata names.
src/Build/Microsoft.Build.csproj Includes new GlobResultBuilder.cs in the build.
src/Build/Evaluation/GlobResultBuilder.cs New shared implementation for deriving GlobResult data from item elements.
src/Build/Evaluation/Evaluator.cs Detects opt-in property, collects relevant item elements, and synthesizes MSBuildItemGlob items.
src/Build/Definition/Project.cs Switches GetAllGlobs to delegate to GlobResultBuilder.
src/Build.UnitTests/Evaluation/ItemGlobs_Tests.cs Adds coverage for synthesized item behavior and escaping edge cases.
documentation/Built-in-Properties.md Documents usage, command-line escaping guidance, and recovery semantics for escaped metadata.

Comment thread src/Build/Evaluation/Evaluator.cs
Comment thread src/Build/Evaluation/Evaluator.cs
Comment thread src/Build/Evaluation/Evaluator.cs
Comment on lines +2762 to +2765
_itemGlobRequestedTypes = new HashSet<string>(
ExpressionShredder.SplitSemiColonSeparatedList(provideProperty.EvaluatedValue),
StringComparer.OrdinalIgnoreCase);
_itemGlobElements = new List<ProjectItemElement>();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This code was just moved, so I'm reluctant to change it in this PR. Leaving this comment unresolved for someone more familiar with MSBuild to comment.

Comment thread src/Build/Evaluation/GlobResultBuilder.cs Outdated

// use immutable builders because there will be a lot of structural sharing between includes which share increasing subsets of corresponding remove elements
// item type -> aggregated information about all removes seen so far for that item type
var removeElementCache = new Dictionary<string, CumulativeRemoveElementData>(projectItemElements.Count);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This code was just moved, so I'm reluctant to change it in this PR. Leaving this comment unresolved for someone more familiar with MSBuild to comment.

@drewnoakes
drewnoakes temporarily deployed to copilot-pat-pool July 16, 2026 04:44 — with GitHub Actions Inactive
@drewnoakes
drewnoakes temporarily deployed to copilot-pat-pool July 16, 2026 04:45 — with GitHub Actions Inactive

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary: This PR introduces a well-designed opt-in feature for synthesizing MSBuildItemGlob items during evaluation. No blocking issues found. Architecture is sound: fully opt-in, zero cost when not set, shares GlobResultBuilder with Project.GetAllGlobs() for identical results, comprehensive test coverage. Minor note: CumulativeRemoveElementData properties call .ToImmutable() on every access which is fragile for future callers.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Expert Code Review (on open) for #14389 · 103.1 AIC · ⌖ 7.33 AIC · ⊞ 4.8K

@drewnoakes
drewnoakes requested a deployment to copilot-pat-pool July 16, 2026 04:45 — with GitHub Actions Abandoned
## Summary

Adds an opt-in mechanism that surfaces a project's *unevaluated* include/exclude/remove glob patterns to targets and tasks during an ordinary command-line build. When the `MSBuildProvideItemGlobs` property is set to a semicolon-separated list of item types (for example `Compile;Content`), the engine synthesizes `MSBuildItemGlob` items during evaluation — one per include element that contributes globs — carrying the patterns in item metadata.

This follows the same pattern as [dotnet#13681](dotnet#13681), which exposed a project's import tree to targets and tasks as `MSBuildImportedProject` items. This change does the equivalent for a project's item globs.

## Motivation

External tooling frequently needs to answer a simple question cheaply and often: *"a file just appeared on disk — does it belong to this project, and to which item type?"* A file watcher, for instance, wants to know whether a newly created `.cs` file is picked up by the project's compile globs without re-running a full evaluation on every file-system event.

Today the only reliable ways to obtain a project's wildcard patterns are to re-evaluate the project or to drive MSBuild through its object model and call `Project.GetAllGlobs()`. Both require hosting the engine in-process, as Visual Studio's project system and the C#/Roslyn language server do. There is no way to extract the same information from a plain `dotnet build` or `msbuild` invocation, where a tool has only the build output to work with.

This change closes that gap using regular MSBuild items. Because they are ordinary items, they flow to every target and task automatically, serialize to out-of-proc worker nodes for free, and require no new public API surface.

## Usage

Set the property to the item types of interest, then read the synthesized items from a target or task:

```xml
<PropertyGroup>
  <MSBuildProvideItemGlobs>Compile;Content</MSBuildProvideItemGlobs>
</PropertyGroup>

<Target Name="ShowGlobs">
  <Message Importance="high"
           Text="%(MSBuildItemGlob.Identity): include=%(MSBuildItemGlob.Include); exclude=%(MSBuildItemGlob.Exclude); remove=%(MSBuildItemGlob.Remove)" />
</Target>
```

The property is intended primarily for tooling that consumes the build output, so it is typically supplied as a global property rather than authored into the project. It can be set in the project file, in a `Directory.Build.props`, through `ProjectCollection.GlobalProperties`, or on the command line.

Each synthesized `MSBuildItemGlob` item represents a single include element and carries:

- `Identity` — the item type (for example `Compile`). Multiple include elements of the same type each produce their own item, so identities repeat and appear in document order.
- `Include` — the include glob pattern(s) of the element, with property references expanded and wildcards preserved.
- `Exclude` — the exclude pattern(s) present on the element (literals and globs).
- `Remove` — the remove pattern(s) that apply to the element (literals and globs).

## How it works

The patterns are derived by exactly the same logic that backs `Project.GetAllGlobs()`. That logic has been lifted out of `Project` into a new shared, generic `GlobResultBuilder`, so the public API and the synthesized items are guaranteed to produce identical results and cannot drift apart.

Synthesis happens in the evaluator at the end of the items pass, in `Evaluator.SynthesizeItemGlobItems`. Doing it during evaluation — rather than during `ProjectInstance` construction — ensures the items appear identically on both `Project` and `ProjectInstance`. Doing it at the *end* of the pass, after all items have been evaluated, ensures that item references (`@(...)`) inside exclude and remove expressions resolve to their final values, matching `GetAllGlobs`.

## Design notes

- **Zero cost when unused.** If `MSBuildProvideItemGlobs` is not set, the feature does no work and allocates nothing.
- **Wildcards are never expanded.** The patterns live in item *metadata*, which is never globbed against the file system, so `**/*.cs` is preserved verbatim rather than being expanded into a file list. Metadata values are standard MSBuild-escaped, semicolon-separated lists, so each pattern round-trips to exactly the string `GetAllGlobs` reports — including patterns containing `%`, and (recovered from the escaped value, as with any MSBuild item list) even the rare pattern containing a literal `;`.
- **Precedence is preserved.** Items are emitted in document order, and each include record carries only the removes that apply to it (those that appear after it in the file). Replaying the records therefore reproduces the authored include/exclude/remove precedence, including "included, then removed, then re-included" sequences where ordering is significant.
- **Literal includes are omitted; literal excludes and removes are kept.** A change to a file that is included by an exact path is always accompanied by a project-file edit, so a watcher re-evaluates anyway. Excludes and removes retain both their literal and glob fragments, which a consumer needs in order to decide membership.

## Tests

`ItemGlobs_Tests` covers opt-out (no items synthesized), opt-in, include/exclude/remove capture, retention of literal excludes and removes, omission of literal includes, item-type filtering, multiple item types, property expansion with wildcard preservation, document order, remove attribution and re-inclusion ordering, conditioned-out elements, the global-property path, `Project` and `ProjectInstance` parity, parity with `Project.GetAllGlobs()`, escaping round-trips for patterns containing `%`, lossless recovery of a pattern that itself contains a literal `;`, and end-to-end availability to a building target.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@drewnoakes
drewnoakes force-pushed the dev/drnoakes/expose-item-globs branch from 80f410a to 0f8aee6 Compare July 16, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants