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,252 @@
// 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);

// Items should not exist on the Project
project.GetItems("MSBuildImportedProject").Count.ShouldBe(0);

// Nor on ProjectInstance
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);

// Items should be visible on Project (created during evaluation)
var projectItems = project.GetItems("MSBuildImportedProject").ToList();
projectItems.Count.ShouldBe(1);
projectItems[0].EvaluatedInclude.ShouldBe(importFile.Path);
projectItems[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path);
projectItems[0].GetMetadataValue("Sdk").ShouldBeEmpty();

// And also on ProjectInstance
ProjectInstance instance = project.CreateProjectInstance();
var instanceItems = instance.GetItems("MSBuildImportedProject").ToList();
instanceItems.Count.ShouldBe(1);
instanceItems[0].EvaluatedInclude.ShouldBe(importFile.Path);
instanceItems[0].GetMetadataValue("ImportingProjectPath").ShouldBe(projectFile.Path);
}

[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);
}
}
}
62 changes: 62 additions & 0 deletions src/Build/Evaluation/Evaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ internal class Evaluator<P, I, M, D>
/// </summary>
private readonly Dictionary<string, ProjectImportElement> _importsSeen;

/// <summary>
/// Resolved imports collected during the depth-first pass, used to optionally synthesize
/// <c>MSBuildImportedProject</c> items at the start of item evaluation.
/// Each entry records the imported file, the importing element, and the SDK result (if any).
/// </summary>
private List<(ProjectRootElement ImportedProject, ProjectImportElement ImportingElement, SdkResult SdkResult)> _resolvedImports;

/// <summary>
/// Depth first collection of InitialTargets strings declared in the main
/// Project and all its imported files, split on semicolons.
Expand Down Expand Up @@ -700,6 +707,9 @@ private void Evaluate()

// Pass3: evaluate project items
MSBuildEventSource.Log.EvaluatePass3Start(projectFile);

SynthesizeImportedProjectItems();

foreach (ProjectItemGroupElement itemGroup in _itemGroupElements)
{
using (_evaluationProfiler.TrackElement(itemGroup))
Expand Down Expand Up @@ -1413,6 +1423,9 @@ private void EvaluateImportElement(string directoryOfImportingFile, ProjectImpor
{
_data.RecordImport(importElement, importedProjectRootElement, importedProjectRootElement.Version, sdkResult);

_resolvedImports ??= [];
_resolvedImports.Add((importedProjectRootElement, importElement, sdkResult));

PerformDepthFirstPass(importedProjectRootElement);
}
}
Expand Down Expand Up @@ -2626,6 +2639,55 @@ private void SetAllProjectsProperty()
}
}

/// <summary>
/// When the <c>MSBuildProvideImportedProjects</c> property is set to <c>true</c>,
/// synthesizes <c>MSBuildImportedProject</c> items from the resolved imports,
/// making the import tree available to targets and tasks as regular items.
/// Called at the beginning of the items pass, after all properties have been evaluated.
/// </summary>
private void SynthesizeImportedProjectItems()
{
if (_resolvedImports is null)
{
return;
}

P provideProperty = _data.GetProperty(Constants.MSBuildProvideImportedProjectsPropertyName);
if (provideProperty is null || !string.Equals(provideProperty.EvaluatedValue, "true", StringComparison.OrdinalIgnoreCase))
{
return;
}

// Create a disconnected item element to back the factory — needed because
// ProjectItemFactory derives ItemType from its backing XML element.
ProjectItemElement syntheticItemElement = ProjectItemElement.CreateDisconnected(
Constants.MSBuildImportedProjectItemType,
_projectRootElement);
_itemFactory.ItemElement = syntheticItemElement;

string definingProject = _projectRootElement.FullPath ?? string.Empty;

foreach (var (importedProject, importingElement, sdkResult) in _resolvedImports)
{
I item = _itemFactory.CreateItem(importedProject.EscapedFullPath ?? string.Empty, definingProject);

ProjectMetadataElement importingPathMetadata = ProjectMetadataElement.CreateDisconnected(
Constants.ImportingProjectPathMetadataName,
_projectRootElement);
item.SetMetadata(importingPathMetadata, importingElement.ContainingProject.EscapedFullPath ?? string.Empty);

if (sdkResult?.SdkReference?.Name is { } sdkName)
{
ProjectMetadataElement sdkMetadata = ProjectMetadataElement.CreateDisconnected(
Constants.SdkMetadataName,
_projectRootElement);
item.SetMetadata(sdkMetadata, sdkName);
}

_data.AddItem(item);
}
}

[Conditional("FEATURE_GUIDE_TO_VS_ON_UNSUPPORTED_PROJECTS")]
private void VerifyVSDistributionPath(string path, ElementLocation importLocationInProject)
{
Expand Down
24 changes: 24 additions & 0 deletions src/Framework/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ 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";

/// <summary>
/// Item type for synthesized import items created when
/// <see cref="MSBuildProvideImportedProjectsPropertyName"/> is set to <c>true</c>.
/// </summary>
internal const string MSBuildImportedProjectItemType = "MSBuildImportedProject";

/// <summary>
/// Metadata name on <see cref="MSBuildImportedProjectItemType"/> items identifying the
/// project file that contains the <c>&lt;Import&gt;</c> element.
/// </summary>
internal const string ImportingProjectPathMetadataName = "ImportingProjectPath";

/// <summary>
/// Metadata name on <see cref="MSBuildImportedProjectItemType"/> items identifying the
/// SDK name when the import was resolved via an SDK reference.
/// </summary>
internal const string SdkMetadataName = "Sdk";

internal const string TaskHostExplicitlyRequested = "TaskHostExplicitlyRequested";
}
}
Loading