diff --git a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
index c31f4db7b21..170cd10a030 100644
--- a/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
+++ b/src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
@@ -14,6 +14,7 @@
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
+using Microsoft.Build.Tasks;
using Microsoft.Build.UnitTests;
using Shouldly;
using Xunit;
@@ -3003,6 +3004,64 @@ public void InvalidProjectReferenceErrorIncludesMultipleReferringProjects()
(mentionsProject1 || mentionsProject2).ShouldBeTrue();
}
+ [Fact]
+ public void Test1()
+ {
+ using var env = TestEnvironment.Create();
+
+ TransientTestFile project1 = env.CreateFile("project1.proj", """
+
+
+ net472;net10.0
+ TargetFramework
+ TargetFrameworks
+
+
+ """);
+
+ TransientTestFile project2 = env.CreateFile("project2.proj", $"""
+
+
+ v4.7.2
+
+
+
+ TargetFramework=net472
+
+
+
+ """);
+
+ var projectGraph = new ProjectGraph(
+ new ProjectGraphOptions
+ {
+ EntryPoints = [new ProjectGraphEntryPoint(project2.Path)],
+ Mode = ProjectGraphMode.Full
+ });
+
+ var sorted = projectGraph.ProjectNodesTopologicallySorted.ToList();
+
+ sorted[0].ProjectInstance.FullPath.ShouldBe(project1.Path);
+ sorted[0].ProjectInstance.GlobalProperties.Count.ShouldBe(2); // IsGraphBuild=true plus TargetFramework=net10.0 (inner build)
+ sorted[0].ProjectReferences.Count.ShouldBe(0);
+ sorted[0].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
+
+ sorted[1].ProjectInstance.FullPath.ShouldBe(project1.Path);
+ sorted[1].ProjectInstance.GlobalProperties.Count.ShouldBe(2); // IsGraphBuild=true plus TargetFramework=net472 (inner build)
+ sorted[1].ProjectReferences.Count.ShouldBe(0);
+ sorted[1].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.InnerBuild);
+
+ sorted[2].ProjectInstance.FullPath.ShouldBe(project1.Path);
+ sorted[2].ProjectInstance.GlobalProperties.Count.ShouldBe(1); // IsGraphBuild=true (outer build)
+ sorted[2].ProjectReferences.Count.ShouldBe(2); // Should have project references to the two inner builds
+ sorted[2].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.OuterBuild);
+
+ sorted[3].ProjectInstance.FullPath.ShouldBe(project2.Path);
+ sorted[3].ProjectInstance.GlobalProperties.Count.ShouldBe(1); // IsGraphBuild=true
+ sorted[3].ProjectReferences.Count.ShouldBe(3); // Should have project references to the outer build of project1 and the two inner builds
+ sorted[3].ProjectType.ShouldBe(ProjectInterpretation.ProjectType.NonMultitargeting);
+ }
+
public void Dispose()
{
_env.Dispose();
diff --git a/src/Build/Graph/GraphBuilder.cs b/src/Build/Graph/GraphBuilder.cs
index fbdfb77a499..19cc92ccda4 100644
--- a/src/Build/Graph/GraphBuilder.cs
+++ b/src/Build/Graph/GraphBuilder.cs
@@ -50,6 +50,7 @@ internal class GraphBuilder
private readonly ProjectInterpretation _projectInterpretation;
private readonly ProjectGraph.ProjectInstanceFactoryFunc _projectInstanceFactory;
+ private readonly ProjectGraphMode _graphMode;
private IReadOnlyDictionary> _solutionDependencies;
private ConcurrentDictionary> _platformNegotiationInstancesCache = new();
@@ -67,6 +68,7 @@ public GraphBuilder(
ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory,
ProjectInterpretation projectInterpretation,
int degreeOfParallelism,
+ ProjectGraphMode mode,
CancellationToken cancellationToken)
{
var (actualEntryPoints, solutionDependencies) = ExpandSolutionIfPresent(entryPoints.ToImmutableArray());
@@ -85,6 +87,7 @@ public GraphBuilder(
_projectCollection = projectCollection;
_projectInstanceFactory = projectInstanceFactory;
_projectInterpretation = projectInterpretation;
+ _graphMode = mode;
}
public void BuildGraph()
@@ -617,7 +620,7 @@ private void SubmitProjectForParsing(ConfigurationMetadata projectToEvaluate)
{
var referenceInfos = new List();
- foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching))
+ foreach (var referenceInfo in _projectInterpretation.GetReferences(parsedProject, _projectCollection, GetInstanceForPlatformNegotiationWithCaching, _graphMode))
{
if (FileUtilities.IsSolutionFilename(referenceInfo.ReferenceConfiguration.ProjectFullPath))
{
diff --git a/src/Build/Graph/ProjectGraph.cs b/src/Build/Graph/ProjectGraph.cs
index d717694d641..0a2a3094bcf 100644
--- a/src/Build/Graph/ProjectGraph.cs
+++ b/src/Build/Graph/ProjectGraph.cs
@@ -420,11 +420,34 @@ public ProjectGraph(
ProjectInstanceFactoryFunc projectInstanceFactory,
int degreeOfParallelism,
CancellationToken cancellationToken)
+ : this(new ProjectGraphOptions
+ {
+ EntryPoints = entryPoints,
+ ProjectCollection = projectCollection,
+ ProjectInstanceFactoryFunc = projectInstanceFactory,
+ DegreeOfParallelism = degreeOfParallelism
+ },
+ cancellationToken)
+ {
+ }
+
+ ///
+ /// Constructs a graph starting from the given graph options.
+ ///
+ /// A containing the entry projects, project collection, and other details about the graph.
+ /// The to observe.
+ /// If the evaluation of any project in the graph fails.
+ /// If the evaluation is successful but the project graph contains a circular dependency.
+ public ProjectGraph(
+ ProjectGraphOptions options,
+ CancellationToken cancellationToken = default)
{
- ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
+ ErrorUtilities.VerifyThrowArgumentNull(options.ProjectCollection);
var measurementInfo = BeginMeasurement();
+ ProjectInstanceFactoryFunc projectInstanceFactory = options.ProjectInstanceFactoryFunc;
+
if (projectInstanceFactory is null)
{
_evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared);
@@ -432,11 +455,12 @@ public ProjectGraph(
}
var graphBuilder = new GraphBuilder(
- entryPoints,
- projectCollection,
+ options.EntryPoints,
+ options.ProjectCollection,
projectInstanceFactory,
ProjectInterpretation.Instance,
- degreeOfParallelism,
+ options.DegreeOfParallelism,
+ options.Mode,
cancellationToken);
graphBuilder.BuildGraph();
@@ -456,7 +480,7 @@ public ProjectGraph(
if (MSBuildEventSource.Log.IsEnabled())
{
- etwArgs = string.Join(";", entryPoints.Select(
+ etwArgs = string.Join(";", options.EntryPoints.Select(
e =>
{
var globalPropertyString = e.GlobalProperties == null
diff --git a/src/Build/Graph/ProjectGraphOptions.cs b/src/Build/Graph/ProjectGraphOptions.cs
new file mode 100644
index 00000000000..61f29859520
--- /dev/null
+++ b/src/Build/Graph/ProjectGraphOptions.cs
@@ -0,0 +1,55 @@
+// 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 Microsoft.Build.Evaluation;
+
+namespace Microsoft.Build.Graph
+{
+ ///
+ /// Represents the mode to use when constructing a .
+ ///
+ public enum ProjectGraphMode
+ {
+ ///
+ /// Loads only the projects needed for a build, as specified by the entry points. This is the default mode.
+ ///
+ Default = 0,
+
+ ///
+ /// Loads a complete representation of the graph, even if they are not needed for the build.
+ ///
+ Full = 1,
+ }
+
+ ///
+ /// Represents options to use when constructing a .
+ ///
+ public readonly struct ProjectGraphOptions()
+ {
+ ///
+ /// The degree of parallelism to use when constructing the graph. Defaults to the number of logical cores on the machine.
+ ///
+ public int DegreeOfParallelism { get; init; } = NativeMethodsShared.GetLogicalCoreCount();
+
+ ///
+ /// A list of objects representing the entry points to use when constructing the graph.
+ ///
+ public required IEnumerable EntryPoints { get; init; }
+
+ ///
+ /// The to use when constructing the graph. Defaults to .
+ ///
+ public ProjectGraphMode Mode { get; init; } = ProjectGraphMode.Default;
+
+ ///
+ /// The to load projects into when constructing the graph. Defaults to .
+ ///
+ public ProjectCollection ProjectCollection { get; init; } = ProjectCollection.GlobalProjectCollection;
+
+ ///
+ /// An optional to use when evaluating individual projects in the graph.
+ ///
+ public ProjectGraph.ProjectInstanceFactoryFunc? ProjectInstanceFactoryFunc { get; init; }
+ }
+}
diff --git a/src/Build/Graph/ProjectInterpretation.cs b/src/Build/Graph/ProjectInterpretation.cs
index 12ae373485d..89acd824f35 100644
--- a/src/Build/Graph/ProjectInterpretation.cs
+++ b/src/Build/Graph/ProjectInterpretation.cs
@@ -74,7 +74,7 @@ public TargetSpecification(string target, bool skipIfNonexistent)
public bool SkipIfNonexistent { get; }
}
- public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory)
+ public IEnumerable GetReferences(ProjectGraphNode projectGraphNode, ProjectCollection projectCollection, ProjectGraph.ProjectInstanceFactoryFunc projectInstanceFactory, ProjectGraphMode graphMode)
{
IEnumerable projectReferenceItems;
IEnumerable globalPropertiesModifiers = null;
@@ -198,6 +198,56 @@ public IEnumerable GetReferences(ProjectGraphNode projectGraphNod
var referenceConfig = new ConfigurationMetadata(projectReferenceFullPath, referenceGlobalProperties);
+ // When in Full graph mode, enumerate all target frameworks regardless of SetTargetFramework
+ if (graphMode == ProjectGraphMode.Full && projectReferenceItem.HasMetadata(SetTargetFrameworkMetadataName))
+ {
+ // Evaluate the referenced project with no global properties to discover all its target frameworks
+ var projectInstanceForDiscovery = projectInstanceFactory(
+ projectReferenceFullPath,
+ null,
+ projectCollection);
+
+ string targetFrameworksValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFrameworks);
+
+ // If the project has multiple target frameworks, yield the outer build only
+ // The normal ConstructInnerBuildReferences logic will discover and create all inner builds
+ if (!string.IsNullOrWhiteSpace(targetFrameworksValue))
+ {
+ // Create base properties without SetTargetFramework constraint
+ // GetGlobalPropertiesForItem processes SetTargetFramework and adds TargetFramework to properties,
+ // so we need to create properties without that processing
+ var baseProperties = GetGlobalPropertiesForItem(
+ projectReferenceItem,
+ requesterInstance.GlobalPropertiesDictionary,
+ allowCollectionReuse: false,
+ // Use empty list to skip ProjectReferenceGlobalPropertiesModifier which processes SetTargetFramework
+ []);
+
+ // Yield only the outer build (no TargetFramework override)
+ // The ConstructInnerBuildReferences logic will discover and create the inner builds
+ yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, baseProperties), projectReferenceItem);
+
+ yield break;
+ }
+ else
+ {
+ string targetFrameworkValue = projectInstanceForDiscovery.GetPropertyValue(PropertyNames.TargetFramework);
+
+ // Single target framework project - just yield it as is without SetTargetFramework constraint
+ if (!string.IsNullOrWhiteSpace(targetFrameworkValue))
+ {
+ var baseProperties = GetGlobalPropertiesForItem(
+ projectReferenceItem,
+ requesterInstance.GlobalPropertiesDictionary,
+ allowCollectionReuse: false,
+ // Use empty list to skip SetTargetFramework processing
+ []);
+ yield return new ReferenceInfo(new ConfigurationMetadata(projectReferenceFullPath, baseProperties), projectReferenceItem);
+ yield break;
+ }
+ }
+ }
+
yield return new ReferenceInfo(referenceConfig, projectReferenceItem);
static void SetProperty(PropertyDictionary properties, string propertyName, string propertyValue)
diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj
index 1162535689a..22876de62da 100644
--- a/src/Build/Microsoft.Build.csproj
+++ b/src/Build/Microsoft.Build.csproj
@@ -211,6 +211,7 @@
+