File: SolutionTests\ProjectDependencyGraphTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.Host.UnitTests
{
    [UseExportProvider]
    [Trait(Traits.Feature, Traits.Features.Workspace)]
    public class ProjectDependencyGraphTests : TestBase
    {
        #region GetTopologicallySortedProjects
 
        [Fact]
        public void TestGetTopologicallySortedProjects()
        {
            VerifyTopologicalSort(CreateSolutionFromReferenceMap("A"), "A");
            VerifyTopologicalSort(CreateSolutionFromReferenceMap("A B"), "AB", "BA");
            VerifyTopologicalSort(CreateSolutionFromReferenceMap("C:A,B B:A A"), "ABC");
            VerifyTopologicalSort(CreateSolutionFromReferenceMap("B:A A C:A D:C,B"), "ABCD", "ACBD");
        }
 
        [Fact]
        public void TestTopologicallySortedProjectsIncrementalUpdate()
        {
            var solution = CreateSolutionFromReferenceMap("A");
 
            VerifyTopologicalSort(solution, "A");
 
            solution = AddProject(solution, "B");
 
            VerifyTopologicalSort(solution, "AB", "BA");
        }
 
        /// <summary>
        /// Verifies that <see cref="ProjectDependencyGraph.GetTopologicallySortedProjects(CancellationToken)"/> 
        /// returns one of the correct results.
        /// </summary>
        /// <param name="solution"></param>
        /// <param name="expectedResults">A list of possible results. Because topological sorting is ambiguous
        /// in that a graph could have multiple topological sorts, this helper lets you give all the possible
        /// results and it asserts that one of them does match.</param>
        private static void VerifyTopologicalSort(Solution solution, params string[] expectedResults)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            var projectIds = projectDependencyGraph.GetTopologicallySortedProjects(CancellationToken.None);
 
            var actualResult = string.Concat(projectIds.Select(id => solution.GetRequiredProject(id).AssemblyName));
            Assert.Contains<string>(actualResult, expectedResults);
        }
 
        #endregion
 
        #region Dependency Sets
 
        [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/542438")]
        public void TestGetDependencySets()
        {
            VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A C:A D E:D F:D"), "ABC DEF");
            VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A,C C"), "ABC");
            VerifyDependencySets(CreateSolutionFromReferenceMap("A B"), "A B");
            VerifyDependencySets(CreateSolutionFromReferenceMap("A B C:B"), "A BC");
            VerifyDependencySets(CreateSolutionFromReferenceMap("A B:A C:A D:B,C"), "ABCD");
        }
 
        [Fact]
        public void TestDependencySetsIncrementalUpdate()
        {
            var solution = CreateSolutionFromReferenceMap("A");
 
            VerifyDependencySets(solution, "A");
 
            solution = AddProject(solution, "B");
 
            VerifyDependencySets(solution, "A B");
        }
 
        private static void VerifyDependencySets(Solution solution, string expectedResult)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            var projectIds = projectDependencyGraph.GetDependencySets(CancellationToken.None);
            var actualResult = string.Join(" ",
                projectIds.Select(
                    group => string.Concat(
                        group.Select(p => solution.GetRequiredProject(p).AssemblyName).OrderBy(n => n))).OrderBy(n => n));
            Assert.Equal(expectedResult, actualResult);
        }
 
        #endregion
 
        #region GetProjectsThatThisProjectTransitivelyDependsOn
 
        [Fact]
        public void TestGetProjectsThatThisProjectTransitivelyDependsOn()
        {
            VerifyTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", []);
            VerifyTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "B", ["A"]);
            VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", ["B", "A"]);
            VerifyTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", []);
        }
 
        [Fact]
        public void TestGetProjectsThatThisProjectTransitivelyDependsOnThrowsArgumentNull()
        {
            var solution = CreateSolutionFromReferenceMap("");
 
            Assert.Throws<ArgumentNullException>("projectId",
                () => solution.GetProjectDependencyGraph().GetProjectsThatThisProjectDirectlyDependsOn(null!));
        }
 
        [Fact]
        public void TestTransitiveReferencesIncrementalUpdateInMiddle()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // but we will add the B -> C link last, to verify that when we add the B to C link we update the references of A.
 
            var solution = CreateSolutionFromReferenceMap("A B C D");
            VerifyTransitiveReferences(solution, "A", []);
            VerifyTransitiveReferences(solution, "B", []);
            VerifyTransitiveReferences(solution, "C", []);
            VerifyTransitiveReferences(solution, "D", []);
 
            solution = AddProjectReferences(solution, "A", new string[] { "B" });
            solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
            VerifyDirectReferences(solution, "A", ["B"]);
            VerifyDirectReferences(solution, "C", ["D"]);
 
            VerifyTransitiveReferences(solution, "A", ["B"]);
            VerifyTransitiveReferences(solution, "B", []);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
 
            solution = AddProjectReferences(solution, "B", new string[] { "C" });
 
            VerifyDirectReferences(solution, "B", ["C"]);
 
            VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
            VerifyTransitiveReferences(solution, "B", ["C", "D"]);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
        }
 
        [Fact]
        public void TestTransitiveReferencesIncrementalUpdateInMiddleLongerChain()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C   D -> E -> F
            //
            // but we will add the C-> D link last, to verify that when we add the C to D link we update the references of A. This is similar
            // to the previous test but with a longer chain.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C D:E E:F F");
            VerifyTransitiveReferences(solution, "A", ["B", "C"]);
            VerifyTransitiveReferences(solution, "B", ["C"]);
            VerifyTransitiveReferences(solution, "D", ["E", "F"]);
            VerifyTransitiveReferences(solution, "E", ["F"]);
 
            solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
            VerifyTransitiveReferences(solution, "A", ["B", "C", "D", "E", "F"]);
        }
 
        [Fact]
        public void TestTransitiveReferencesIncrementalUpdateWithReferencesAlreadyTransitivelyIncluded()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C
            //
            // and then we'll add a reference from A -> C, and transitive references should be different
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C");
 
            void VerifyAllTransitiveReferences()
            {
                VerifyTransitiveReferences(solution, "A", ["B", "C"]);
                VerifyTransitiveReferences(solution, "B", ["C"]);
                VerifyTransitiveReferences(solution, "C", []);
            }
 
            VerifyAllTransitiveReferences();
            VerifyDirectReferences(solution, "A", ["B"]);
 
            solution = AddProjectReferences(solution, "A", new string[] { "C" });
 
            VerifyAllTransitiveReferences();
            VerifyDirectReferences(solution, "A", ["B", "C"]);
        }
 
        [Fact]
        public void TestTransitiveReferencesIncrementalUpdateWithProjectThatHasUnknownReferences()
        {
            // We are going to create a solution with the references:
            //
            // A  B  C -> D
            //
            // and then we will add a link from A to B. We won't ask for transitive references first,
            // so we shouldn't have any information for A, B, or C and have to deal with that.
 
            var solution = CreateSolutionFromReferenceMap("A B C:D D");
            solution = solution.WithProjectReferences(solution.GetProjectsByName("C").Single().Id, []);
 
            VerifyTransitiveReferences(solution, "A", []);
 
            // At this point, we know the references for "A" (it's empty), but B and C's are still unknown.
            // At this point, we're also going to directly use the underlying project graph APIs;
            // the higher level solution APIs often call and ask for transitive information as well, which makes
            // this particularly hard to test -- it turns out the data we think is uncomputed might be computed prior
            // to adding the reference.
            var dependencyGraph = solution.GetProjectDependencyGraph();
            var projectAId = solution.GetProjectsByName("A").Single().Id;
            var projectBId = solution.GetProjectsByName("B").Single().Id;
            dependencyGraph = dependencyGraph.WithAdditionalProjectReferences(projectAId, [new ProjectReference(projectBId)]);
 
            VerifyTransitiveReferences(solution, dependencyGraph, project: "A", expectedResults: ["B"]);
        }
 
        [Fact]
        public void TestTransitiveReferencesWithDanglingProjectReference()
        {
            // We are going to create a solution with the references:
            //
            // A -> B
            //
            // but we're going to add A as a reference with B not existing yet. Then we'll add in B and ask.
 
            var solution = CreateSolution();
            var projectAId = ProjectId.CreateNewId("A");
            var projectBId = ProjectId.CreateNewId("B");
 
            var projectAInfo = ProjectInfo.Create(projectAId, VersionStamp.Create(), "A", "A", LanguageNames.CSharp,
                                    projectReferences: [new ProjectReference(projectBId)]);
 
            solution = solution.AddProject(projectAInfo);
 
            VerifyDirectReferences(solution, "A", []);
            VerifyTransitiveReferences(solution, "A", []);
 
            solution = solution.AddProject(projectBId, "B", "B", LanguageNames.CSharp);
 
            VerifyTransitiveReferences(solution, "A", ["B"]);
        }
 
        [Fact]
        public void TestTransitiveReferencesWithMultipleReferences()
        {
            // We are going to create a solution with the references:
            //
            // A    B -> C    D -> E
            //
            // and then add A referencing B and D in one call, to make sure that works.
 
            var solution = CreateSolutionFromReferenceMap("A B:C C D:E E");
            VerifyTransitiveReferences(solution, "A", []);
 
            solution = AddProjectReferences(solution, "A", new string[] { "B", "D" });
 
            VerifyDirectReferences(solution, "A", ["B", "D"]);
            VerifyTransitiveReferences(solution, "A", ["B", "C", "D", "E"]);
        }
 
        private static void VerifyDirectReferences(Solution solution, string project, string[] expectedResults)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            var projectId = solution.GetProjectsByName(project).Single().Id;
            var projectIds = projectDependencyGraph.GetProjectsThatThisProjectDirectlyDependsOn(projectId);
 
            var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
            Assert.Equal<string>(
                expectedResults.OrderBy(n => n),
                actualResults.OrderBy(n => n));
        }
 
        private static void VerifyTransitiveReferences(Solution solution, string project, string[] expectedResults)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            VerifyTransitiveReferences(solution, projectDependencyGraph, project, expectedResults);
        }
 
        private static void VerifyTransitiveReferences(Solution solution, ProjectDependencyGraph projectDependencyGraph, string project, string[] expectedResults)
        {
            var projectId = solution.GetProjectsByName(project).Single().Id;
            var projectIds = projectDependencyGraph.GetProjectsThatThisProjectTransitivelyDependsOn(projectId);
 
            var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
            Assert.Equal<string>(
                expectedResults.OrderBy(n => n),
                actualResults.OrderBy(n => n));
        }
 
        #endregion
 
        [Fact]
        public void TestDirectAndReverseDirectReferencesAfterWithProjectReferences()
        {
            var solution = CreateSolutionFromReferenceMap("A:B B");
 
            VerifyDirectReverseReferences(solution, "B", ["A"]);
 
            solution = solution.WithProjectReferences(solution.GetProjectsByName("A").Single().Id,
                []);
 
            VerifyDirectReferences(solution, "A", []);
            VerifyDirectReverseReferences(solution, "B", []);
        }
 
        #region GetProjectsThatTransitivelyDependOnThisProject
 
        [Fact]
        public void TestGetProjectsThatTransitivelyDependOnThisProject()
        {
            VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("A"), "A", []);
            VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("B:A A"), "A", ["B"]);
            VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "A", ["B", "C"]);
            VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("C:B B:A A"), "C", []);
            VerifyReverseTransitiveReferences(CreateSolutionFromReferenceMap("D:C,B B:A C A"), "A", ["D", "B"]);
        }
 
        [Fact]
        public void TestGetProjectsThatTransitivelyDependOnThisProjectThrowsArgumentNull()
        {
            var solution = CreateSolutionFromReferenceMap("");
 
            Assert.Throws<ArgumentNullException>("projectId",
                () => solution.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(null!));
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesIncrementalUpdateInMiddle()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // but we will add the B -> C link last, to verify that when we add the B to C link we update the reverse references of D.
 
            var solution = CreateSolutionFromReferenceMap("A B C D");
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", []);
            VerifyReverseTransitiveReferences(solution, "C", []);
            VerifyReverseTransitiveReferences(solution, "D", []);
 
            solution = AddProjectReferences(solution, "A", new string[] { "B" });
            solution = AddProjectReferences(solution, "C", new string[] { "D" });
 
            VerifyDirectReverseReferences(solution, "B", ["A"]);
            VerifyDirectReverseReferences(solution, "D", ["C"]);
 
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", []);
            VerifyReverseTransitiveReferences(solution, "D", ["C"]);
 
            solution = AddProjectReferences(solution, "B", new string[] { "C" });
 
            VerifyDirectReverseReferences(solution, "C", ["B"]);
 
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
            VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesForUnrelatedProjectAfterWithProjectReferences()
        {
            // We are going to create a solution with the references:
            //
            // A -> B       C -> D
            //
            // and will then remove the reference from C to D. This process will cause us to throw out
            // all our caches, and asking for the reverse references of A will compute it again.
 
            var solution = CreateSolutionFromReferenceMap("A:B B C:D D");
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", []);
            VerifyReverseTransitiveReferences(solution, "D", ["C"]);
 
            solution = solution.WithProjectReferences(solution.GetProjectsByName("C").Single().Id, []);
 
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
        }
 
        [Fact]
        public void TestForwardReferencesAfterProjectRemoval()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // and will then remove project B.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
            VerifyDirectReferences(solution, "A", ["B"]);
            VerifyDirectReferences(solution, "B", ["C"]);
            VerifyDirectReferences(solution, "C", ["D"]);
            VerifyDirectReferences(solution, "D", []);
 
            solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
            VerifyDirectReferences(solution, "A", []);
            VerifyDirectReferences(solution, "C", ["D"]);
            VerifyDirectReferences(solution, "D", []);
        }
 
        [Fact]
        public void TestForwardTransitiveReferencesAfterProjectRemoval()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // and will then remove project B.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
            VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
            VerifyTransitiveReferences(solution, "B", ["C", "D"]);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
 
            solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
            VerifyTransitiveReferences(solution, "A", []);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
        }
 
        [Fact]
        public void TestReverseReferencesAfterProjectRemoval()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // and will then remove project B.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
            VerifyDirectReverseReferences(solution, "A", []);
            VerifyDirectReverseReferences(solution, "B", ["A"]);
            VerifyDirectReverseReferences(solution, "C", ["B"]);
            VerifyDirectReverseReferences(solution, "D", ["C"]);
 
            solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
            VerifyDirectReverseReferences(solution, "A", []);
            VerifyDirectReverseReferences(solution, "C", []);
            VerifyDirectReverseReferences(solution, "D", ["C"]);
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesAfterProjectRemoval()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //
            // and will then remove project B.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C:D D");
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
            VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
 
            solution = solution.RemoveProject(solution.GetProjectsByName("B").Single().Id);
 
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "C", []);
            VerifyReverseTransitiveReferences(solution, "D", ["C"]);
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_PreserveThroughUnrelatedSequence()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C
            //   \
            //    > D
            //
            // and will then remove the project reference A->B. This test verifies that the new project dependency graph
            // did not lose previously-computed information about the transitive reverse references for D.
 
            var solution = CreateSolutionFromReferenceMap("A:B,D B:C C D");
            VerifyReverseTransitiveReferences(solution, "D", ["A"]);
 
            var a = solution.GetProjectsByName("A").Single();
            var b = solution.GetProjectsByName("B").Single();
            var d = solution.GetProjectsByName("D").Single();
            var expected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(d.Id);
 
            var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
            solution = solution.RemoveProjectReference(a.Id, aToB);
 
            // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
            // non-null set. Specifically, it returns the _same_ set that was computed prior to the project reference
            // removal.
            Assert.Same(expected, solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(d.Id));
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_PreserveUnrelated()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C
            // D -> E
            //
            // and will then remove the project reference A->B. This test verifies that the new project dependency graph
            // did not lose previously-computed information about the transitive reverse references for E.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C C D:E E");
            VerifyReverseTransitiveReferences(solution, "E", ["D"]);
 
            var a = solution.GetProjectsByName("A").Single();
            var b = solution.GetProjectsByName("B").Single();
            var e = solution.GetProjectsByName("E").Single();
            var expected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(e.Id);
 
            var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
            solution = solution.RemoveProjectReference(a.Id, aToB);
 
            // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
            // non-null set. Specifically, it returns the _same_ set that was computed prior to the project reference
            // removal.
            Assert.Same(expected, solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(e.Id));
        }
 
        [Fact]
        public void TestReverseTransitiveReferencesAfterProjectReferenceRemoval_DiscardImpacted()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C
            //   \
            //    > D
            //
            // and will then remove the project reference A->B. This test verifies that the new project dependency graph
            // discards previously-computed information about the transitive reverse references for C.
 
            var solution = CreateSolutionFromReferenceMap("A:B,D B:C C D");
            VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
 
            var a = solution.GetProjectsByName("A").Single();
            var b = solution.GetProjectsByName("B").Single();
            var c = solution.GetProjectsByName("C").Single();
            var notExpected = solution.SolutionState.GetProjectDependencyGraph().GetProjectsThatTransitivelyDependOnThisProject(c.Id);
            Assert.NotNull(notExpected);
 
            var aToB = a.ProjectReferences.Single(reference => reference.ProjectId == b.Id);
            solution = solution.RemoveProjectReference(a.Id, aToB);
 
            // Before any other operations, verify that TryGetProjectsThatTransitivelyDependOnThisProject returns a
            // null set.
            Assert.Null(solution.SolutionState.GetProjectDependencyGraph().GetTestAccessor().TryGetProjectsThatTransitivelyDependOnThisProject(c.Id));
            VerifyReverseTransitiveReferences(solution, "C", ["B"]);
        }
 
        [Fact]
        public void TestSameDependencyGraphAfterOneOfMultipleReferencesRemoved()
        {
            // We are going to create a solution with the references:
            //
            // A -> B -> C -> D
            //        \__^
            //
            // This solution has multiple references from B->C. We will remove one reference from B->C and verify that
            // the project dependency graph in the solution state did not change (reference equality).
            //
            // We then remove the second reference, and verify that the dependency graph does change.
 
            var solution = CreateSolutionFromReferenceMap("A:B B:C,C C:D D");
 
            VerifyDirectReferences(solution, "A", ["B"]);
            VerifyDirectReferences(solution, "B", ["C"]);
            VerifyDirectReferences(solution, "C", ["D"]);
            VerifyDirectReferences(solution, "D", []);
 
            VerifyTransitiveReferences(solution, "A", ["B", "C", "D"]);
            VerifyTransitiveReferences(solution, "B", ["C", "D"]);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
 
            VerifyDirectReverseReferences(solution, "A", []);
            VerifyDirectReverseReferences(solution, "B", ["A"]);
            VerifyDirectReverseReferences(solution, "C", ["B"]);
            VerifyDirectReverseReferences(solution, "D", ["C"]);
 
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", ["A", "B"]);
            VerifyReverseTransitiveReferences(solution, "D", ["A", "B", "C"]);
 
            var dependencyGraph = solution.SolutionState.GetProjectDependencyGraph();
            Assert.NotNull(dependencyGraph);
 
            var b = solution.GetProjectsByName("B").Single();
            var c = solution.GetProjectsByName("C").Single();
            var firstBToC = b.ProjectReferences.First(reference => reference.ProjectId == c.Id);
            solution = solution.RemoveProjectReference(b.Id, firstBToC);
            Assert.Same(dependencyGraph, solution.SolutionState.GetProjectDependencyGraph());
 
            b = solution.GetProjectsByName("B").Single();
            var remainingBToC = b.ProjectReferences.Single(reference => reference.ProjectId == c.Id);
            solution = solution.RemoveProjectReference(b.Id, remainingBToC);
            Assert.NotSame(dependencyGraph, solution.SolutionState.GetProjectDependencyGraph());
 
            VerifyDirectReferences(solution, "A", ["B"]);
            VerifyDirectReferences(solution, "B", []);
            VerifyDirectReferences(solution, "C", ["D"]);
            VerifyDirectReferences(solution, "D", []);
 
            VerifyTransitiveReferences(solution, "A", ["B"]);
            VerifyTransitiveReferences(solution, "B", []);
            VerifyTransitiveReferences(solution, "C", ["D"]);
            VerifyTransitiveReferences(solution, "D", []);
 
            VerifyDirectReverseReferences(solution, "A", []);
            VerifyDirectReverseReferences(solution, "B", ["A"]);
            VerifyDirectReverseReferences(solution, "C", []);
            VerifyDirectReverseReferences(solution, "D", ["C"]);
 
            VerifyReverseTransitiveReferences(solution, "A", []);
            VerifyReverseTransitiveReferences(solution, "B", ["A"]);
            VerifyReverseTransitiveReferences(solution, "C", []);
            VerifyReverseTransitiveReferences(solution, "D", ["C"]);
        }
 
        private static void VerifyDirectReverseReferences(Solution solution, string project, string[] expectedResults)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            var projectId = solution.GetProjectsByName(project).Single().Id;
            var projectIds = projectDependencyGraph.GetProjectsThatDirectlyDependOnThisProject(projectId);
 
            var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
            Assert.Equal<string>(
                expectedResults.OrderBy(n => n),
                actualResults.OrderBy(n => n));
        }
 
        private static void VerifyReverseTransitiveReferences(Solution solution, string project, string[] expectedResults)
        {
            var projectDependencyGraph = solution.GetProjectDependencyGraph();
            var projectId = solution.GetProjectsByName(project).Single().Id;
            var projectIds = projectDependencyGraph.GetProjectsThatTransitivelyDependOnThisProject(projectId);
 
            var actualResults = projectIds.Select(id => solution.GetRequiredProject(id).Name);
 
            Assert.Equal<string>(
                expectedResults.OrderBy(n => n),
                actualResults.OrderBy(n => n));
        }
 
        #endregion
 
        #region Helpers
 
        private static Solution CreateSolutionFromReferenceMap(string projectReferences)
        {
            var solution = CreateSolution();
 
            var references = new Dictionary<string, IEnumerable<string>>();
 
            var projectDefinitions = projectReferences.Split(' ');
            foreach (var projectDefinition in projectDefinitions)
            {
                var projectDefinitionParts = projectDefinition.Split(':');
                string[]? referencedProjectNames = null;
 
                if (projectDefinitionParts.Length == 2)
                {
                    referencedProjectNames = projectDefinitionParts[1].Split(',');
                }
                else if (projectDefinitionParts.Length != 1)
                {
                    throw new ArgumentException("Invalid project definition: " + projectDefinition);
                }
 
                var projectName = projectDefinitionParts[0];
                if (referencedProjectNames != null)
                {
                    references.Add(projectName, referencedProjectNames);
                }
 
                solution = AddProject(solution, projectName);
            }
 
            foreach (var (name, refs) in references)
                solution = AddProjectReferences(solution, name, refs);
 
            return solution;
        }
 
        private static Solution AddProject(Solution solution, string projectName)
        {
            var projectId = ProjectId.CreateNewId(debugName: projectName);
            return solution.AddProject(ProjectInfo.Create(projectId, VersionStamp.Create(), projectName, projectName, LanguageNames.CSharp, projectName));
        }
 
        private static Solution AddProjectReferences(Solution solution, string projectName, IEnumerable<string> projectReferences)
        {
            var referencesByTargetProject = new Dictionary<string, List<ProjectReference>>();
            foreach (var targetProject in projectReferences)
            {
                var references = referencesByTargetProject.GetOrAdd(targetProject, _ => []);
                if (references.Count == 0)
                {
                    references.Add(new ProjectReference(solution.GetProjectsByName(targetProject).Single().Id));
                }
                else
                {
                    references.Add(new ProjectReference(solution.GetProjectsByName(targetProject).Single().Id, [$"alias{references.Count}"]));
                }
            }
 
            return solution.AddProjectReferences(
                solution.GetProjectsByName(projectName).Single().Id,
                referencesByTargetProject.SelectMany(pair => pair.Value));
        }
 
        private static Solution CreateSolution()
            => new AdhocWorkspace().CurrentSolution;
 
        #endregion
    }
}