Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions documentation/Built-in-Properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ However, properties set there are not available at all parts of execution, and s

Reserved properties are [set by the toolset][toolset_reservedproperties] and are available _only_ in the `.tasks` and `.overridetasks` cases. Properties set there are not available in normal project evaluation.

## Synthesized import items

When the property `MSBuildProvideImportedProjects` is set to `true`, the engine synthesizes `MSBuildImportedProject` items during `ProjectInstance` construction. Each item represents a file imported during evaluation, with:

- **Identity** — the full path of the imported file.
- **`ImportingProjectPath`** metadata — the full path of the file containing the `<Import>` element.
- **`Sdk`** metadata — the SDK name if the import was resolved via an SDK reference (e.g. `Microsoft.NET.Sdk`); empty otherwise.

The property can be set in the project file or passed as a global property (e.g. `/p:MSBuildProvideImportedProjects=true`). The items are regular MSBuild items, so they serialize to out-of-proc worker nodes and are available to any target or task. Projects that don't set the property pay zero cost.

Each imported file appears at most once (first occurrence in depth-first evaluation order), so the collection forms a tree. The root project itself is excluded — only actual import relationships are represented.

Implementation: items are added in [`ProjectInstance.CreateImportsSnapshot()`][createimportssnapshot] from the evaluated import closure.

[createimportssnapshot]: https://github.com/dotnet/msbuild/blob/main/src/Build/Instance/ProjectInstance.cs

[addbuiltinproperties]: https://github.com/dotnet/msbuild/blob/24b33188f385cee07804cc63ec805216b3f8b72f/src/Build/Evaluation/Evaluator.cs#L609-L612

[setbuiltinproperty]: https://github.com/dotnet/msbuild/blob/24b33188f385cee07804cc63ec805216b3f8b72f/src/Build/Evaluation/Evaluator.cs#L1257
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Build.Definition;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Unittest;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;

namespace Microsoft.Build.UnitTests.OM.Instance
{
/// <summary>
/// Tests for the MSBuildImportedProject items synthesized from the import closure.
/// </summary>
public class ProjectInstance_ImportedProjectItems_Tests
{
private readonly ITestOutputHelper _output;

public ProjectInstance_ImportedProjectItems_Tests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public void ImportedProjectItemsNotCreatedWithoutOptIn()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string importContent = "<Project />";
var importFile = env.CreateFile("import.targets", importContent);

string projectContent = $"""
<Project>
<Import Project="{importFile.Path}" />
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0);
}

[Fact]
public void ImportedProjectItemsCreatedWhenPropertyIsSet()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string importContent = "<Project />";
var importFile = env.CreateFile("import.targets", importContent);

string projectContent = $"""
<Project>
<PropertyGroup>
<MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects>
</PropertyGroup>
<Import Project="{importFile.Path}" />
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

var items = instance.GetItems("MSBuildImportedProject").ToList();
items.Count.ShouldBe(1);
items[0].EvaluatedInclude.ShouldBe(importFile.Path);
items[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path);
items[0].GetMetadataValue("Sdk").ShouldBeEmpty();
}

[Fact]
public void ImportedProjectItemsHaveCorrectImportingPath()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string import2Content = "<Project />";
var import2File = env.CreateFile("import2.targets", import2Content);

string import1Content = $"""
<Project>
<Import Project="{import2File.Path}" />
</Project>
""";
var import1File = env.CreateFile("import1.targets", import1Content);

string projectContent = $"""
<Project>
<PropertyGroup>
<MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects>
</PropertyGroup>
<Import Project="{import1File.Path}" />
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

var items = instance.GetItems("MSBuildImportedProject").ToList();
items.Count.ShouldBe(2);

// project -> import1
var item1 = items.First(i => i.EvaluatedInclude == import1File.Path);
item1.GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path);

// import1 -> import2
var item2 = items.First(i => i.EvaluatedInclude == import2File.Path);
item2.GetMetadataValue("ImportingProjectPath").ShouldBe(import1File.Path);
}

[Fact]
public void ImportedProjectItemsExcludeRootProject()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string projectContent = """
<Project>
<PropertyGroup>
<MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects>
</PropertyGroup>
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

instance.GetItems("MSBuildImportedProject").Count.ShouldBe(0);
}

[Fact]
public void ImportedProjectItemsAvailableToTargets()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string importContent = "<Project />";
var importFile = env.CreateFile("import.targets", importContent);

string projectContent = $"""
<Project>
<PropertyGroup>
<MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects>
</PropertyGroup>
<Import Project="{importFile.Path}" />
<Target Name="ShowImports">
<Message Text="Import: %(MSBuildImportedProject.Identity) from %(MSBuildImportedProject.ImportingProjectPath)" Importance="High" />
</Target>
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties: null, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

var mockLogger = new MockLogger(_output);
instance.Build(["ShowImports"], [mockLogger]).ShouldBeTrue();
mockLogger.AssertLogContains($"Import: {importFile.Path} from {projectFile.Path}");
}

[Fact]
public void ImportedProjectItemsHaveSdkMetadataForSdkImports()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string testSdkDirectory = env.CreateFolder().Path;
File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.props"), "<Project />");
File.WriteAllText(Path.Combine(testSdkDirectory, "Sdk.targets"), "<Project />");

var projectOptions = SdkUtilities.CreateProjectOptionsWithResolver(
new SdkUtilities.FileBasedMockSdkResolver(new Dictionary<string, string>
{
{ "MyTestSdk", testSdkDirectory },
}));

string projectContent = """
<Project Sdk="MyTestSdk">
<PropertyGroup>
<MSBuildProvideImportedProjects>true</MSBuildProvideImportedProjects>
</PropertyGroup>
</Project>
""";
Comment thread
drewnoakes marked this conversation as resolved.

using ProjectRootElementFromString projectRootElementFromString = new(projectContent);
Project project = Project.FromProjectRootElement(
projectRootElementFromString.Project,
projectOptions);
ProjectInstance instance = project.CreateProjectInstance();

var items = instance.GetItems("MSBuildImportedProject").ToList();
items.Count.ShouldBe(2); // Sdk.props and Sdk.targets

// Both should have Sdk metadata set to "MyTestSdk"
foreach (var item in items)
{
item.GetMetadataValue("Sdk").ShouldBe("MyTestSdk");
}
}

[Fact]
public void ImportedProjectItemsCreatedWhenSetViaGlobalProperty()
{
using TestEnvironment env = TestEnvironment.Create(_output);

string importContent = "<Project />";
var importFile = env.CreateFile("import.targets", importContent);

string projectContent = $"""
<Project>
<Import Project="{importFile.Path}" />
</Project>
""";
var projectFile = env.CreateFile("test.proj", projectContent);

var globalProperties = new Dictionary<string, string>
{
{ "MSBuildProvideImportedProjects", "true" },
};

using var collection = new ProjectCollection();
var project = new Project(projectFile.Path, globalProperties, toolsVersion: null, collection);
ProjectInstance instance = project.CreateProjectInstance();

var items = instance.GetItems("MSBuildImportedProject").ToList();
items.Count.ShouldBe(1);
items[0].EvaluatedInclude.ShouldBe(importFile.Path);
items[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path);
}
}
}
36 changes: 36 additions & 0 deletions src/Build/Instance/ProjectInstance.cs
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not convinced this is the right place for the code. From the conversation with @rainersigwald, my understanding was that we'd place this in the evaluation stage: at the start of the items pass, after the property evaluation pass. This change runs after evaluation, during ProjectInstance construction, which means the synthesized items only appear on ProjectInstance, not on Project. I am not entirely sure how important this distinction is for your use case, but I can imagine there being cases (edge or otherwise) where it could make a difference.

Original file line number Diff line number Diff line change
Expand Up @@ -3376,6 +3376,42 @@ private void CreateImportsSnapshot(IList<ResolvedImport> importClosure, IList<Re

_importPathsIncludingDuplicates = importPathsIncludingDuplicates;
ImportPathsIncludingDuplicates = importPathsIncludingDuplicates.AsReadOnly();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer to make this a separate helper method instead, if we would keep this code here.

// Optionally synthesize MSBuildImportedProject items from the import closure,
// making the import tree available to targets and tasks as regular items.
if (string.Equals(
_properties?.GetProperty(Constants.MSBuildProvideImportedProjectsPropertyName)?.EvaluatedValue,
"true",
StringComparison.OrdinalIgnoreCase))
{
Comment thread
drewnoakes marked this conversation as resolved.
List<KeyValuePair<string, string>> metadata = null;

foreach (ResolvedImport import in importClosure)
{
if (import.ImportingElement is null)
{
// Skip the outer project itself, which is not an import.
continue;
}

metadata ??= [];
metadata.Clear();

metadata.Add(new("ImportingProjectPath", import.ImportingElement.ContainingProject.EscapedFullPath));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's make the constants for item and metadata names as well.


if (import.SdkResult?.SdkReference?.Name is { } sdkName)
{
metadata.Add(new("Sdk", sdkName));
}

_items.Add(new ProjectItemInstance(
project: this,
itemType: "MSBuildImportedProject",
includeEscaped: import.ImportedProject.EscapedFullPath,
directMetadata: metadata,
definingFileEscaped: EscapingUtilities.Escape(FullPath)));
}
Comment thread
drewnoakes marked this conversation as resolved.
}
Comment thread
drewnoakes marked this conversation as resolved.
}

/// <summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Framework/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ internal static class Constants

internal const string MSBuildAllProjectsPropertyName = "MSBuildAllProjects";

/// <summary>
/// Name of the MSBuild property that opts in to synthesizing <c>MSBuildImportedProject</c>
/// items from the import closure during <c>ProjectInstance</c> creation.
/// </summary>
internal const string MSBuildProvideImportedProjectsPropertyName = "MSBuildProvideImportedProjects";

internal const string TaskHostExplicitlyRequested = "TaskHostExplicitlyRequested";
}
}
30 changes: 30 additions & 0 deletions src/MSBuild/MSBuild/Microsoft.Build.CommonTypes.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,31 @@ elementFormDefault="qualified">
</xs:annotation>
</xs:element>
<xs:element name="Service" type="msb:SimpleItemType" substitutionGroup="msb:Item"/>
<xs:element name="MSBuildImportedProject" substitutionGroup="msb:Item">
<xs:annotation>
<xs:documentation><!-- _locID_text="MSBuildImportedProject" _locComment="" -->Synthesized by the engine when MSBuildProvideImportedProjects is true. Each item represents an imported project file.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:complexContent>
<xs:extension base="msb:SimpleItemType">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:choice>
<xs:element name="ImportingProjectPath">
<xs:annotation>
<xs:documentation><!-- _locID_text="MSBuildImportedProject_ImportingProjectPath" _locComment="" -->Full path of the project file that contains the Import element.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Sdk">
<xs:annotation>
<xs:documentation><!-- _locID_text="MSBuildImportedProject_Sdk" _locComment="" -->The SDK name if this import was resolved via an SDK reference (e.g. "Microsoft.NET.Sdk"); empty otherwise.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
</xs:element>
<xs:element name="WebReferences" type="msb:SimpleItemType" substitutionGroup="msb:Item">
<xs:annotation>
<xs:documentation><!-- _locID_text="WebReferences" _locComment="" -->Name of Web References folder to display in user interface</xs:documentation>
Expand Down Expand Up @@ -1916,6 +1941,11 @@ elementFormDefault="qualified">
</xs:element>
<xs:element name="MyType" type="msb:StringPropertyType" substitutionGroup="msb:Property"/>
<xs:element name="MSBuildAllProjects" type="msb:StringPropertyType" substitutionGroup="msb:Property"/>
<xs:element name="MSBuildProvideImportedProjects" type="msb:StringPropertyType" substitutionGroup="msb:Property">
<xs:annotation>
<xs:documentation><!-- _locID_text="MSBuildProvideImportedProjects" _locComment="" -->When set to 'true', the engine synthesizes MSBuildImportedProject items representing the import tree of the project. Each item's identity is the full path of an imported file, with ImportingProjectPath and Sdk metadata.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="MSBuildTreatWarningsAsErrors" type="msb:StringPropertyType" substitutionGroup="msb:Property">
<xs:annotation>
<xs:documentation><!-- _locID_text="MSBuildTreatWarningsAsErrors" _locComment="" -->Indicates whether to treat all warnings as errors when building a project.</xs:documentation>
Expand Down