File: Utility\MSBuildAPIUtility.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.CommandLine.XPlat\NuGet.CommandLine.XPlat.csproj (NuGet.CommandLine.XPlat)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using NuGet.Common;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;
using NuGet.Protocol.Core.Types;

namespace NuGet.CommandLine.XPlat
{
    internal class MSBuildAPIUtility
    {
        private const string PACKAGE_REFERENCE_TYPE_TAG = "PackageReference";
        private const string PACKAGE_VERSION_TYPE_TAG = "PackageVersion";
        private const string VERSION_TAG = "Version";
        private const string FRAMEWORK_TAG = "TargetFramework";
        private const string FRAMEWORKS_TAG = "TargetFrameworks";
        private const string RESTORE_STYLE_TAG = "RestoreProjectStyle";
        private const string NUGET_STYLE_TAG = "NuGetProjectStyle";
        private const string ASSETS_FILE_PATH_TAG = "ProjectAssetsFile";
        private const string UPDATE_OPERATION = "Update";
        private const string REMOVE_OPERATION = "Remove";
        private const string IncludeAssets = "IncludeAssets";
        private const string PrivateAssets = "PrivateAssets";
        private const string CollectPackageReferences = "CollectPackageReferences";
        private const string CollectCentralPackageVersions = "CollectCentralPackageVersions";

        /// <summary>
        /// The name of the MSBuild property that represents the path to the central package management file, usually Directory.Packages.props.
        /// </summary>
        private const string DirectoryPackagesPropsPathPropertyName = "DirectoryPackagesPropsPath";

        public ILogger Logger { get; }

        public IVirtualProjectBuilder VirtualProjectBuilder { get; }

        public MSBuildAPIUtility(ILogger logger, IVirtualProjectBuilder virtualProjectBuilder)
        {
            Logger = logger ?? throw new ArgumentNullException(nameof(logger));
            VirtualProjectBuilder = virtualProjectBuilder;
        }

        /// <summary>
        /// Opens an MSBuild.Evaluation.Project type from a csproj file.
        /// </summary>
        /// <param name="projectCSProjPath">CSProj file which needs to be evaluated</param>
        internal SaveableProject GetProject(string projectCSProjPath)
        {
            var (projectRootElement, isVirtual) = TryOpenProjectRootElement(projectCSProjPath);
            if (projectRootElement is null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_MsBuildUnableToOpenProject, projectCSProjPath));
            }
            return new SaveableProject { Project = new Project(projectRootElement), VirtualProject = isVirtual ? (projectCSProjPath, VirtualProjectBuilder) : null };
        }

        /// <summary>
        /// Opens an MSBuild.Evaluation.Project type from a csproj file with the given global properties.
        /// </summary>
        /// <param name="projectCSProjPath">CSProj file which needs to be evaluated</param>
        /// <param name="globalProperties">Global properties that should be used to evaluate the project while opening.</param>
        private SaveableProject GetProject(string projectCSProjPath, IDictionary<string, string> globalProperties)
        {
            var (projectRootElement, isVirtual) = TryOpenProjectRootElement(projectCSProjPath);
            if (projectRootElement is null)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_MsBuildUnableToOpenProject, projectCSProjPath));
            }
            return new SaveableProject { Project = new Project(projectRootElement, globalProperties, toolsVersion: null), VirtualProject = isVirtual ? (projectCSProjPath, VirtualProjectBuilder) : null };
        }

        private static bool IsCentralPackageManagementEnabled(Project project)
        {
            return StringComparer.OrdinalIgnoreCase.Equals(
                project.GetPropertyValue("ManagePackageVersionsCentrally"),
                "true");
        }

        internal static IEnumerable<string> GetProjectsFromSolution(string solutionPath)
        {
            var sln = SolutionFile.Parse(solutionPath);

            if (XPlatUtility.IsSolutionFile(solutionPath))
            {
                return sln.ProjectsInOrder.Select(p => p.AbsolutePath);
            }

            MethodInfo projectShouldBuildMethod = typeof(SolutionFile).GetMethod("ProjectShouldBuild", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            Func<string, bool> projectShouldBuild = (Func<string, bool>)Delegate.CreateDelegate(typeof(Func<string, bool>), sln, projectShouldBuildMethod);

            List<string> projects = new List<string>();
            foreach (var project in sln.ProjectsInOrder)
            {
                if (projectShouldBuild(project.RelativePath))
                {
                    projects.Add(project.AbsolutePath);
                }
            }

            return projects;
        }

        /// <summary>
        /// Get the list of project paths from the input 'path' argument. Path must be a directory, solution file or project file.
        /// </summary>
        /// <returns>List of project paths. Returns null if path was a directory with none or multiple project/solution files.</returns>
        /// <exception cref="ArgumentException">Throws an exception if the directory has none or multiple project/solution files.</exception>
        internal IEnumerable<string> GetListOfProjectsFromPathArgument(string path)
        {
            string fullPath = Path.GetFullPath(path);

            if (VirtualProjectBuilder?.IsValidEntryPointPath(fullPath) == true)
            {
                return [fullPath];
            }

            string projectOrSolutionFile;

            // the path points to a directory
            if (Directory.Exists(fullPath))
            {
                projectOrSolutionFile = XPlatUtility.GetProjectOrSolutionFileFromDirectory(fullPath);
            }
            // the path points to a project or solution file
            else if (XPlatUtility.IsSolutionFile(fullPath) || XPlatUtility.IsProjectFile(fullPath))
            {
                projectOrSolutionFile = fullPath;
            }
            else
            {
                throw new ArgumentException(
                    string.Format(
                        CultureInfo.CurrentCulture,
                        Strings.Error_PathIsMissingOrInvalid,
                        path));
            }

            return XPlatUtility.IsSolutionFile(projectOrSolutionFile)
                        ? GetProjectsFromSolution(projectOrSolutionFile).Where(File.Exists)
                        : [projectOrSolutionFile];
        }

        /// <summary>
        /// Remove all package references to the project.
        /// </summary>
        /// <param name="projectPath">Path to the csproj file of the project.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be removed.</param>
        public int RemovePackageReference(string projectPath, LibraryDependency libraryDependency)
        {
            var project = GetProject(projectPath);

            var existingPackageReferences = project.Project.ItemsIgnoringCondition
                .Where(item => item.ItemType.Equals(PACKAGE_REFERENCE_TYPE_TAG, StringComparison.OrdinalIgnoreCase) &&
                               item.EvaluatedInclude.Equals(libraryDependency.Name, StringComparison.OrdinalIgnoreCase));

            if (existingPackageReferences.Any())
            {
                // We validate that the operation does not remove any imported items
                // If it does then we throw a user friendly exception without making any changes
                ValidateNoImportedItemsAreUpdated(existingPackageReferences, libraryDependency, REMOVE_OPERATION);

                project.Project.RemoveItems(existingPackageReferences);
                project.Save();
                ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project);

                return 0;
            }
            else
            {
                Logger.LogError(string.Format(CultureInfo.CurrentCulture,
                    Strings.Error_UpdatePkgNoSuchPackage,
                    project.Project.FullPath,
                    libraryDependency.Name,
                    REMOVE_OPERATION));
                ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project);

                return 1;
            }
        }

        /// <summary>
        /// Check if the project files format are correct for CPM
        /// </summary>
        /// <param name="packageReferenceArgs">Arguments used in the command</param>
        /// <param name="packageSpec"></param>
        /// <returns></returns>
        public bool AreCentralVersionRequirementsSatisfied(PackageReferenceArgs packageReferenceArgs, PackageSpec packageSpec)
        {
            var project = GetProject(packageReferenceArgs.ProjectPath).Project;
            string directoryPackagesPropsPath = project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName);

            // Get VersionOverride if it exisits in the package reference.
            IEnumerable<LibraryDependency> dependenciesWithVersionOverride = null;

            if (packageSpec.RestoreMetadata.CentralPackageVersionOverrideDisabled)
            {
                dependenciesWithVersionOverride = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => !d.AutoReferenced && d.VersionOverride != null));
                // Emit a error if VersionOverride was specified for a package reference but that functionality is disabled
                foreach (var item in dependenciesWithVersionOverride)
                {
                    packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_VersionOverrideDisabled, string.Join(";", dependenciesWithVersionOverride.Select(d => d.Name))));
                    return false;
                }
            }

            // The dependencies should not have versions explicitly defined if cpvm is enabled.
            IEnumerable<LibraryDependency> dependenciesWithDefinedVersion = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => !d.VersionCentrallyManaged && !d.AutoReferenced && d.VersionOverride == null));
            if (dependenciesWithDefinedVersion.Any())
            {
                packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_VersionsNotAllowed, string.Join(";", dependenciesWithDefinedVersion.Select(d => d.Name))));
                return false;
            }
            IEnumerable<LibraryDependency> autoReferencedAndDefinedInCentralFile = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => d.AutoReferenced && tfm.CentralPackageVersions.ContainsKey(d.Name)));
            if (autoReferencedAndDefinedInCentralFile.Any())
            {
                packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_AutoreferencedReferencesNotAllowed, string.Join(";", autoReferencedAndDefinedInCentralFile.Select(d => d.Name))));
                return false;
            }
            IEnumerable<LibraryDependency> packageReferencedDependenciesWithoutCentralVersionDefined = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Where(d => d.LibraryRange.VersionRange == null));
            if (packageReferencedDependenciesWithoutCentralVersionDefined.Any())
            {
                packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_MissingPackageVersion, string.Join(";", packageReferencedDependenciesWithoutCentralVersionDefined.Select(d => d.Name))));
                return false;
            }

            if (!packageSpec.RestoreMetadata.CentralPackageFloatingVersionsEnabled)
            {
                var floatingVersionDependencies = packageSpec.TargetFrameworks.SelectMany(tfm => tfm.CentralPackageVersions.Values).Where(cpv => cpv.VersionRange.IsFloating);
                if (floatingVersionDependencies.Any())
                {
                    packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_CentralPackageVersions_FloatingVersionsAreNotAllowed));
                    return false;
                }
            }

            // PackageVersion should not be defined outside the project file.
            var packageVersions = project.Items.Where(item => item.ItemType == PACKAGE_VERSION_TYPE_TAG && item.EvaluatedInclude.Equals(packageReferenceArgs.PackageId) && !item.Xml.ContainingProject.FullPath.Equals(directoryPackagesPropsPath));
            if (packageVersions.Any())
            {
                packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkg_CentralPackageVersions_PackageVersion_WrongLocation, packageReferenceArgs.PackageId));
                return false;
            }

            // PackageReference should not be defined in Directory.Packages.props
            var packageReferenceOutsideProjectFile = project.Items.Where(item => item.ItemType == PACKAGE_REFERENCE_TYPE_TAG && item.Xml.ContainingProject.FullPath.Equals(directoryPackagesPropsPath));
            if (packageReferenceOutsideProjectFile.Any())
            {
                packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkg_CentralPackageVersions_PackageReference_WrongLocation, packageReferenceArgs.PackageId));
                return false;
            }

            ProjectItem packageReference = project.Items.LastOrDefault(item => item.ItemType == PACKAGE_REFERENCE_TYPE_TAG && item.EvaluatedInclude.Equals(packageReferenceArgs.PackageId));
            ProjectItem packageVersionInProps = packageVersions.LastOrDefault();
            var versionOverride = dependenciesWithVersionOverride?.FirstOrDefault(d => d.Name.Equals(packageReferenceArgs.PackageId));

            // If package reference exists and the user defined a VersionOverride or PackageVersions but didn't specified a version, no-op
            if (packageReference != null && (versionOverride != null || packageVersionInProps != null) && packageReferenceArgs.NoVersion)
            {
                return false;
            }

            ProjectCollection.GlobalProjectCollection.UnloadProject(project);
            return true;
        }

        /// <summary>
        /// Add an unconditional package reference to the project.
        /// </summary>
        /// <param name="projectPath">Path to the csproj file of the project.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="noVersion">If a version is passed in as a CLI argument.</param>
        public void AddPackageReference(string projectPath, LibraryDependency libraryDependency, bool noVersion)
        {
            var project = GetProject(projectPath);

            // Here we get package references for any framework.
            // If the project has a conditional reference, then an unconditional reference is not added.

            var existingPackageReferences = GetPackageReferencesForAllFrameworks(project, libraryDependency);
            AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion);
            ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project);
        }

        /// <summary>
        /// Add conditional package reference to the project per target framework.
        /// </summary>
        /// <param name="projectPath">Path to the csproj file of the project.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="frameworks">Target Frameworks for which the package reference should be added.</param>
        /// <param name="noVersion">If a version is passed in as a CLI argument.</param>
        public void AddPackageReferencePerTFM(string projectPath, LibraryDependency libraryDependency,
            IEnumerable<string> frameworks, bool noVersion)
        {
            foreach (var framework in frameworks)
            {
                var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                { { "TargetFramework", framework } };
                var project = GetProject(projectPath, globalProperties);
                var existingPackageReferences = GetPackageReferences(project.Project, libraryDependency);
                AddPackageReference(project, libraryDependency, existingPackageReferences, noVersion, framework);
                ProjectCollection.GlobalProjectCollection.UnloadProject(project.Project);
            }
        }

        /// <summary>
        /// Add package version/package reference to the solution/project based on if the project has been onboarded to CPM or not.
        /// </summary>
        /// <param name="project">Project that needs to be modified.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="existingPackageReferences">Package references that already exist in the project.</param>
        /// <param name="noVersion">If a version is passed in as a CLI argument.</param>
        /// <param name="framework">Target Framework for which the package reference should be added.</param>
        private void AddPackageReference(SaveableProject project,
            LibraryDependency libraryDependency,
            IEnumerable<ProjectItem> existingPackageReferences,
            bool noVersion,
            string framework = null)
        {
            // Determine CPM status from the loaded project so callers don't need to check separately.
            bool isCentralPackageManagementEnabled = IsCentralPackageManagementEnabled(project.Project);

            // Add packageReference to the project file only if it does not exist.
            if (!isCentralPackageManagementEnabled)
            {
                if (!existingPackageReferences.Any())
                {
                    //Modify the project file.
                    ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project.Project);
                    AddPackageReferenceIntoItemGroup(itemGroup, libraryDependency);
                }
                else
                {
                    // If the package already has a reference then try to update the reference.
                    UpdatePackageReferenceItems(existingPackageReferences, libraryDependency);
                }
            }
            else
            {
                // Get package version and VersionOverride if it already exists in the props file. Returns null if there is no matching package version.
                ProjectItem packageReferenceInProps = project.Project.Items.LastOrDefault(i => i.ItemType == PACKAGE_REFERENCE_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name));
                var versionOverrideExists = packageReferenceInProps?.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride") && !string.IsNullOrWhiteSpace(i.EvaluatedValue));

                if (!existingPackageReferences.Any())
                {
                    //Add <PackageReference/> to the project file.
                    ProjectItemGroupElement itemGroup = GetOrCreateItemGroup(framework, project.Project);
                    AddPackageReferenceIntoItemGroupCPM(project.Project, itemGroup, libraryDependency);
                }

                if (versionOverrideExists != null)
                {
                    // Update if VersionOverride instead of Directory.Packages.props file
                    string packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString;
                    UpdateVersionOverride(project, packageReferenceInProps, packageVersion);
                }
                else
                {
                    // Get package version if it already exists in the props file. Returns null if there is no matching package version.
                    ProjectItem packageVersionInProps = project.Project.Items.LastOrDefault(i => i.ItemType == PACKAGE_VERSION_TYPE_TAG && i.EvaluatedInclude.Equals(libraryDependency.Name));

                    // If no <PackageVersion /> exists in the Directory.Packages.props file.
                    if (packageVersionInProps == null)
                    {
                        // Modifying the props file if project is onboarded to CPM.
                        AddPackageVersionIntoItemGroupCPM(project.Project, libraryDependency);
                    }
                    else
                    {
                        // Modify the Directory.Packages.props file with the version that is passed in.
                        if (!noVersion)
                        {
                            string packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString;
                            UpdatePackageVersion(project, packageVersionInProps, packageVersion);
                        }

                    }
                }
            }

            project.Save();

            static ProjectItemGroupElement GetOrCreateItemGroup(string targetFrameworkAlias, Project project)
            {
                // Getting all the item groups in a given project
                var itemGroups = GetItemGroups(project);
                string condition = targetFrameworkAlias is null ? null : GetTargetFrameworkCondition(targetFrameworkAlias);
                var itemGroup = GetItemGroup(itemGroups, PACKAGE_REFERENCE_TYPE_TAG, condition) ?? CreateItemGroup(project, condition);
                return itemGroup;
            }
        }

        /// <summary>
        /// Add package name and version using PackageVersion tag for projects onboarded to CPM.
        /// </summary>
        /// <param name="project">Project that needs to be modified.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        private void AddPackageVersionIntoItemGroupCPM(Project project, LibraryDependency libraryDependency)
        {
            // If onboarded to CPM get the directoryBuildPropsRootElement.
            ProjectRootElement directoryBuildPropsRootElement = GetDirectoryBuildPropsRootElement(project);

            // Get the ItemGroup to add a PackageVersion to or create a new one.
            var propsItemGroup = GetItemGroup(directoryBuildPropsRootElement.ItemGroups, PACKAGE_VERSION_TYPE_TAG, condition: null) ?? directoryBuildPropsRootElement.AddItemGroup();
            AddPackageVersionIntoPropsItemGroup(propsItemGroup, libraryDependency);

            // Save the updated props file.
            Debug.Assert(directoryBuildPropsRootElement.ContainingProject.FullPath != project.FullPath);
            directoryBuildPropsRootElement.Save();
        }

        /// <summary>
        /// Get the Directory build props root element for projects onboarded to CPM.
        /// </summary>
        /// <param name="project">Project that needs to be modified.</param>
        /// <returns>The directory build props root element.</returns>
        internal static ProjectRootElement GetDirectoryBuildPropsRootElement(Project project)
        {
            // Get the Directory.Packages.props path.
            string directoryPackagesPropsPath = project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName);
            ProjectRootElement directoryBuildPropsRootElement = project.Imports.FirstOrDefault(i => i.ImportedProject.FullPath.Equals(directoryPackagesPropsPath, PathUtility.GetStringComparisonBasedOnOS())).ImportedProject;
            return directoryBuildPropsRootElement;
        }

        /// <summary>
        /// Add package name and version into the props file.
        /// Only version metadata belongs on PackageVersion items; asset metadata (PrivateAssets, IncludeAssets)
        /// belongs on the PackageReference item in the project file.
        /// </summary>
        /// <param name="itemGroup">Item group that needs to be modified in the props file.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        internal void AddPackageVersionIntoPropsItemGroup(ProjectItemGroupElement itemGroup,
            LibraryDependency libraryDependency)
        {
            var item = itemGroup.AddItem(PACKAGE_VERSION_TYPE_TAG, libraryDependency.Name);
            var packageVersion = AddVersionMetadata(libraryDependency, item);
            Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgAdded, libraryDependency.Name, packageVersion, itemGroup.ContainingProject.FullPath
            ));
        }

        /// <summary>
        /// Add package name and version into item group.
        /// </summary>
        /// <param name="itemGroup">Item group to add to.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        private void AddPackageReferenceIntoItemGroup(ProjectItemGroupElement itemGroup,
            LibraryDependency libraryDependency)
        {
            // Add both package reference information and version metadata using the PACKAGE_REFERENCE_TYPE_TAG.
            var item = itemGroup.AddItem(PACKAGE_REFERENCE_TYPE_TAG, libraryDependency.Name);
            var packageVersion = AddVersionMetadata(libraryDependency, item);
            AddExtraMetadataToProjectItemElement(libraryDependency, item);
            Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgAdded, libraryDependency.Name, packageVersion, itemGroup.ContainingProject.FullPath));
        }

        /// <summary>
        /// Add only the package name into the project file for projects onboarded to CPM.
        /// </summary>
        /// <param name="project">Project to be modified.</param>
        /// <param name="itemGroup">Item group to add to.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        internal void AddPackageReferenceIntoItemGroupCPM(Project project, ProjectItemGroupElement itemGroup,
            LibraryDependency libraryDependency)
        {
            // Only add the package reference information using the PACKAGE_REFERENCE_TYPE_TAG.
            ProjectItemElement item = itemGroup.AddItem(PACKAGE_REFERENCE_TYPE_TAG, libraryDependency.Name);
            AddExtraMetadataToProjectItemElement(libraryDependency, item);
            Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgCPM, libraryDependency.Name, itemGroup.ContainingProject.FullPath, project.GetPropertyValue(DirectoryPackagesPropsPathPropertyName)));
        }

        /// <summary>
        /// Add other metadata based on certain flags.
        /// </summary>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="item">Item to add the metadata to.</param>
        private static void AddExtraMetadataToProjectItemElement(LibraryDependency libraryDependency, ProjectItemElement item)
        {
            if (libraryDependency.IncludeType != LibraryIncludeFlags.All)
            {
                var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType));
                item.AddMetadata(IncludeAssets, includeFlags, expressAsAttribute: false);
            }

            if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent)
            {
                var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent));
                item.AddMetadata(PrivateAssets, suppressParent, expressAsAttribute: false);
            }
        }

        /// <summary>
        /// Get all the item groups in a given project.
        /// </summary>
        /// <param name="project">A specified project.</param>
        /// <returns></returns>
        internal static IEnumerable<ProjectItemGroupElement> GetItemGroups(Project project)
        {
            return project
                .Items
                .Where(i => !i.IsImported)
                .Select(item => item.Xml.Parent as ProjectItemGroupElement)
                .Distinct();
        }

        /// <summary>
        /// Get an itemGroup that will contains a package reference tag and meets the condition.
        /// </summary>
        /// <param name="itemGroups">List of all item groups in the project</param>
        /// <param name="itemType">An item type tag that must be in the item group. It if PackageReference in this case.</param>
        /// <param name="condition">The condition that the Item Group must have. Use <see langword="null" /> for no condition.</param>
        /// <returns>An ItemGroup, which could be null.</returns>
        internal static ProjectItemGroupElement GetItemGroup(IEnumerable<ProjectItemGroupElement> itemGroups,
            string itemType,
            string condition)
        {
            var itemGroup = itemGroups?
                .Where(itemGroupElement => itemGroupElement.Items.Any(item => item.ItemType == itemType))?
                .Where(itemGroupElement => condition is null || itemGroupElement.Condition == condition)
                .FirstOrDefault();

            return itemGroup;
        }

        /// <summary>
        /// Creating an item group in a project.
        /// </summary>
        /// <param name="project">Project where the item group should be created.</param>
        /// <param name="condition">The condition to be put on the Item Group. Use <see langword="null" /> for no condition.</param>
        /// <returns>An Item Group.</returns>
        internal static ProjectItemGroupElement CreateItemGroup(Project project, string condition)
        {
            // Create a new item group and add a condition if given
            var itemGroup = project.Xml.AddItemGroup();
            if (condition != null)
            {
                itemGroup.Condition = condition;
            }
            return itemGroup;
        }

        /// <summary>
        /// Adding version metadata to a given project item element.
        /// </summary>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="item">The item that the version metadata should be added to.</param>
        /// <returns>The package version that is added in the metadata.</returns>
        private static string AddVersionMetadata(LibraryDependency libraryDependency, ProjectItemElement item)
        {
            var packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString ??
                    libraryDependency.LibraryRange.VersionRange.MinVersion.ToString();

            ProjectMetadataElement versionAttribute = item.Metadata.FirstOrDefault(i => i.Name.Equals("Version", StringComparison.OrdinalIgnoreCase));

            // If version attribute does not exist at all, add it.
            if (versionAttribute == null)
            {
                item.AddMetadata(VERSION_TAG, packageVersion, expressAsAttribute: true);
            }
            // Else, just update the version in the already existing version attribute.
            else
            {
                versionAttribute.Value = packageVersion;
            }
            return packageVersion;
        }

        /// <summary>
        /// Update package references for a project that is not onboarded to CPM.
        /// </summary>
        /// <param name="packageReferencesItems">Existing package references.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        private void UpdatePackageReferenceItems(IEnumerable<ProjectItem> packageReferencesItems,
           LibraryDependency libraryDependency)
        {
            // We validate that the operation does not update any imported items
            // If it does then we throw a user friendly exception without making any changes
            ValidateNoImportedItemsAreUpdated(packageReferencesItems, libraryDependency, UPDATE_OPERATION);

            foreach (var packageReferenceItem in packageReferencesItems)
            {
                var packageVersion = libraryDependency.LibraryRange.VersionRange.OriginalString ??
                    libraryDependency.LibraryRange.VersionRange.MinVersion.ToString();

                packageReferenceItem.SetMetadataValue(VERSION_TAG, packageVersion);

                UpdateExtraMetadataInProjectItem(libraryDependency, packageReferenceItem);

                Logger.LogInformation(string.Format(CultureInfo.CurrentCulture,
                    Strings.Info_AddPkgUpdated,
                    libraryDependency.Name,
                    packageVersion,
                    packageReferenceItem.Xml.ContainingProject.FullPath));
            }
        }

        /// <summary>
        /// Updates VersionOverride from <PackageReference /> element if version is passed in as a CLI argument
        /// </summary>
        /// <param name="project"></param>
        /// <param name="packageReference"></param>
        /// <param name="versionCLIArgument"></param>
        internal static void UpdateVersionOverride(SaveableProject project, ProjectItem packageReference, string versionCLIArgument)
        {
            // Determine where the <PackageVersion /> item is decalred
            ProjectItemElement packageReferenceItemElement = project.Project.GetItemProvenance(packageReference).LastOrDefault()?.ItemElement;

            // Get the Version attribute on the packageVersionItemElement.
            ProjectMetadataElement versionOverrideAttribute = packageReferenceItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("VersionOverride"));

            // Update the version
            versionOverrideAttribute.Value = versionCLIArgument;
            project.Save(packageReferenceItemElement.ContainingProject);
        }

        /// <summary>
        /// Update the <PackageVersion /> element if a version is passed in as a CLI argument.
        /// </summary>
        /// <param name="project"></param>
        /// <param name="packageVersion"><PackageVersion /> item with a matching package ID.</param>
        /// <param name="versionCLIArgument">Version that is passed in as a CLI argument.</param>
        internal static void UpdatePackageVersion(SaveableProject project, ProjectItem packageVersion, string versionCLIArgument)
        {
            // Determine where the <PackageVersion /> item is decalred
            ProjectItemElement packageVersionItemElement = project.Project.GetItemProvenance(packageVersion).LastOrDefault()?.ItemElement;

            // Get the Version attribute on the packageVersionItemElement.
            ProjectMetadataElement versionAttribute = packageVersionItemElement.Metadata.FirstOrDefault(i => i.Name.Equals("Version", StringComparison.OrdinalIgnoreCase));
            // Update the version
            versionAttribute.Value = versionCLIArgument;
            project.Save(packageVersionItemElement.ContainingProject);
        }

        /// <summary>
        /// Validate that no imported items in the project are updated with the package version.
        /// </summary>
        /// <param name="packageReferencesItems">Existing package reference items.</param>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="operationType">Operation types such as if a package reference is being updated.</param>
        /// <exception cref="InvalidOperationException"></exception>
        private static void ValidateNoImportedItemsAreUpdated(IEnumerable<ProjectItem> packageReferencesItems,
            LibraryDependency libraryDependency,
            string operationType)
        {
            var importedPackageReferences = packageReferencesItems
                .Where(i => i.IsImported)
                .ToArray();

            // Throw if any of the package references to be updated are imported.
            if (importedPackageReferences.Any())
            {
                var errors = new StringBuilder();
                foreach (var importedPackageReference in importedPackageReferences)
                {
                    errors.AppendLine(string.Format(CultureInfo.CurrentCulture,
                        "\t " + Strings.Error_AddPkgErrorStringForImportedEdit,
                        importedPackageReference.ItemType,
                        importedPackageReference.UnevaluatedInclude,
                        importedPackageReference.Xml.ContainingProject.FullPath));
                }
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                    Strings.Error_AddPkgFailOnImportEdit,
                    operationType,
                    libraryDependency.Name,
                    Environment.NewLine,
                    errors));
            }
        }

        /// <summary>
        /// Update other metadata for items based on certain flags.
        /// </summary>
        /// <param name="libraryDependency">Package Dependency of the package to be added.</param>
        /// <param name="packageReferenceItem">Item to be modified.</param>
        private static void UpdateExtraMetadataInProjectItem(LibraryDependency libraryDependency, ProjectItem packageReferenceItem)
        {
            if (libraryDependency.IncludeType != LibraryIncludeFlags.All)
            {
                var includeFlags = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.IncludeType));
                packageReferenceItem.SetMetadataValue(IncludeAssets, includeFlags);
            }

            if (libraryDependency.SuppressParent != LibraryIncludeFlagUtils.DefaultSuppressParent)
            {
                var suppressParent = MSBuildStringUtility.Convert(LibraryIncludeFlagUtils.GetFlagString(libraryDependency.SuppressParent));
                packageReferenceItem.SetMetadataValue(PrivateAssets, suppressParent);
            }
        }

        /// <summary>
        /// A simple check for some of the evaluated properties to check
        /// if the project is package reference project or not
        /// </summary>
        /// <param name="project"></param>
        /// <returns></returns>
        internal static bool IsPackageReferenceProject(Project project)
        {
            return (project.GetPropertyValue(RESTORE_STYLE_TAG) == "PackageReference" ||
                    project.GetItems(PACKAGE_REFERENCE_TYPE_TAG).Count != 0 ||
                    project.GetPropertyValue(NUGET_STYLE_TAG) == "PackageReference" ||
                    project.GetPropertyValue(ASSETS_FILE_PATH_TAG) != "");
        }

        /// <summary>
        /// Prepares the dictionary that maps frameworks to packages top-level
        /// and transitive.
        /// </summary>
        /// <param name="project">Project to get the resoled versions from</param>
        /// <param name="userInputFrameworks">A list of frameworks</param>
        /// <param name="assetsFile">Assets file for all targets and libraries</param>
        /// <param name="transitive">Include transitive packages/projects in the result</param>
        /// <returns>FrameworkPackages collection with top-level and transitive package/project
        /// references for each framework, or null on error</returns>
        internal static List<FrameworkPackages> GetResolvedVersions(
            Project project, IEnumerable<string> userInputFrameworks, LockFile assetsFile, bool transitive)
        {
            if (userInputFrameworks == null)
            {
                throw new ArgumentNullException(nameof(userInputFrameworks));
            }

            if (project == null)
            {
                throw new ArgumentNullException(nameof(project));
            }

            if (assetsFile == null)
            {
                throw new ArgumentNullException(nameof(assetsFile));
            }

            var resultPackages = new List<FrameworkPackages>();
            var requestedTargetFrameworks = assetsFile.PackageSpec.TargetFrameworks;
            var requestedTargets = assetsFile.Targets;

            // If the user has entered frameworks, we want to filter
            // the targets and frameworks from the assets file
            if (userInputFrameworks.Any())
            {
                //Target frameworks filtering
                var userFrameworkInputs = userInputFrameworks.Select(f =>
                                               f.Split(['/'], StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray()[0]).ToList();
                requestedTargetFrameworks = requestedTargetFrameworks.Where(tfm =>
                    userFrameworkInputs.Any(f => string.Equals(f, tfm.TargetAlias, StringComparison.OrdinalIgnoreCase))).ToList();

                //Assets file targets filtering by framework and RID
                var filteredTargets = new List<LockFileTarget>();
                foreach (var frameworkAndRID in userInputFrameworks)
                {
                    var splitFrameworkAndRID = frameworkAndRID.Split(['/'], StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
                    // If a / is not present in the string, we get all of the targets that
                    // have matching framework regardless of RID.
                    if (splitFrameworkAndRID.Length == 1)
                    {
                        filteredTargets.AddRange(requestedTargets.Where(target =>
                            string.Equals(target.TargetAlias, splitFrameworkAndRID[0], StringComparison.OrdinalIgnoreCase)));
                    }
                    else
                    {
                        //RID is present in the user input, so we filter using it as well
                        filteredTargets.AddRange(requestedTargets.Where(target =>
                            string.Equals(target.TargetAlias, splitFrameworkAndRID[0], StringComparison.OrdinalIgnoreCase) &&
                            target.RuntimeIdentifier != null && target.RuntimeIdentifier.Equals(splitFrameworkAndRID[1], StringComparison.OrdinalIgnoreCase)));
                    }
                }
                requestedTargets = filteredTargets;
            }

            // Filtering the Targets to ignore TargetFramework + RID combination, only keep TargetFramework in requestedTargets.
            // So that only one section will be shown for each TFM.
            requestedTargets = requestedTargets.Where(target => target.RuntimeIdentifier == null).ToList();

            foreach (var target in requestedTargets)
            {
                // Find the tfminformation corresponding to the target to
                // get the top-level dependencies
                TargetFrameworkInformation tfmInformation;

                try
                {
                    tfmInformation = requestedTargetFrameworks.First(tfm => tfm.TargetAlias.Equals(target.TargetAlias));
                }
                catch (Exception)
                {
                    throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingAssetsFile, assetsFile.Path));
                }

                //The packages for the framework that were retrieved with GetRequestedVersions
                var frameworkDependencies = tfmInformation.Dependencies;
                var targetAlias = tfmInformation.TargetAlias;
                var projectPackages = GetPackageReferencesFromTargets(project, targetAlias);
                var topLevelPackages = new List<InstalledPackageReference>();
                var transitivePackages = new List<InstalledPackageReference>();

                foreach (var library in target.Libraries)
                {
                    var matchingPackages = frameworkDependencies.Where(d =>
                        d.Name.Equals(library.Name, StringComparison.OrdinalIgnoreCase)).ToList();

                    var resolvedVersion = library.Version.ToString();

                    //In case we found a matching package in requestedVersions, the package will be
                    //top level.
                    if (matchingPackages.Any())
                    {
                        var topLevelPackage = matchingPackages.Single();
                        InstalledPackageReference installedPackage = default;

                        //If the package is not auto-referenced, get the version from the project file. Otherwise fall back on the assets file
                        if (!topLevelPackage.AutoReferenced)
                        {
                            try
                            { // In case proj and assets file are not in sync and some refs were deleted
                                installedPackage = projectPackages.First(p => p.Name.Equals(topLevelPackage.Name, StringComparison.Ordinal));
                            }
                            catch (Exception)
                            {
                                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ListPkg_ErrorReadingReferenceFromProject, project.FullPath));
                            }
                        }
                        else
                        {
                            var projectFileVersion = topLevelPackage.LibraryRange.VersionRange.ToString();
                            installedPackage = new InstalledPackageReference(library.Name)
                            {
                                OriginalRequestedVersion = projectFileVersion
                            };
                        }

                        installedPackage.ResolvedPackageMetadata = PackageSearchMetadataBuilder
                            .FromIdentity(new PackageIdentity(library.Name, library.Version))
                            .Build();

                        installedPackage.AutoReference = topLevelPackage.AutoReferenced;

                        if (library.Type != "project")
                        {
                            topLevelPackages.Add(installedPackage);
                        }
                    }
                    // If no matching packages were found, then the package is transitive,
                    // and include-transitive must be used to add the package
                    else if (transitive) // be sure to exclude "project" references here as these are irrelevant
                    {
                        var installedPackage = new InstalledPackageReference(library.Name)
                        {
                            ResolvedPackageMetadata = PackageSearchMetadataBuilder
                                .FromIdentity(new PackageIdentity(library.Name, library.Version))
                                .Build()
                        };

                        if (library.Type != "project")
                        {
                            transitivePackages.Add(installedPackage);
                        }
                    }
                }

                var frameworkPackages = new FrameworkPackages(
                    target.TargetFramework.GetShortFolderName(),
                    targetAlias,
                    topLevelPackages,
                    transitivePackages);

                resultPackages.Add(frameworkPackages);
            }

            return resultPackages;
        }


        /// <summary>
        /// Returns all package references after evaluating the condition on the item groups.
        /// This method is used when we need package references for a specific target framework.
        /// </summary>
        /// <param name="project">Project for which the package references have to be obtained.</param>
        /// <param name="packageId">Name of the package. If empty, returns all package references</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package references</returns>
        private static IEnumerable<ProjectItem> GetPackageReferences(Project project, string packageId)
        {

            var packageReferences = project.AllEvaluatedItems
                .Where(item => item.ItemType.Equals(PACKAGE_REFERENCE_TYPE_TAG, StringComparison.OrdinalIgnoreCase));

            if (string.IsNullOrEmpty(packageId))
            {
                return packageReferences;
            }

            return packageReferences
                .Where(item => item.EvaluatedInclude.Equals(packageId, StringComparison.OrdinalIgnoreCase));
        }

        /// <summary>
        /// Returns all package references after evaluating the condition on the item groups.
        /// This method is used when we need package references for a specific target framework.
        /// </summary>
        /// <param name="project">Project for which the package references have to be obtained.</param>
        /// <param name="libraryDependency">Library dependency to get the name of the package</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package references</returns>
        private static IEnumerable<ProjectItem> GetPackageReferences(Project project, LibraryDependency libraryDependency)
        {
            return GetPackageReferences(project, libraryDependency.Name);
        }

        /// <summary>
        /// Returns all package references after invoking the target CollectPackageReferences.
        /// </summary>
        /// <param name="project">The project for which the package references have to be obtained.</param>
        /// <param name="framework">Framework to get reference(s) for</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package references</returns>
        private static IEnumerable<InstalledPackageReference> GetPackageReferencesFromTargets(Project project, string framework)
        {
            var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                {
                    { "TargetFramework", framework },
                    { "ExcludeRestorePackageImports", bool.TrueString }
                };
            var newProject = new ProjectInstance(project.Xml, globalProperties, null, ProjectCollection.GlobalProjectCollection);
            newProject.Build(new[] { CollectPackageReferences, CollectCentralPackageVersions }, new List<Microsoft.Build.Framework.ILogger> { }, out var targetOutputs);

            // Find the first target output that matches `CollectPackageReferences`
            var matchingTargetOutputReference = targetOutputs.First(
                e => e.Key.Equals(CollectPackageReferences, StringComparison.OrdinalIgnoreCase)
            );

            // Target that matches `CollectCentralPackageVersions`. This will be used to get the versions of `GlobalPackageReference` packages
            var matchingTargetOutputVersion = targetOutputs.First(
                e => e.Key.Equals(CollectCentralPackageVersions, StringComparison.OrdinalIgnoreCase)
            );

            var referenceItems = matchingTargetOutputReference.Value.Items;
            var versionItems = matchingTargetOutputVersion.Value.Items;

            // Transform each item into an InstalledPackageReference
            var installedPackageReferences = referenceItems.Select(p =>
            {
                // Find the matching version item for the current reference item
                var versionItem = versionItems.FirstOrDefault(v =>
                    v.ItemSpec.Equals(p.ItemSpec, StringComparison.OrdinalIgnoreCase)
                );

                // Check if there is a version override
                bool isVersionOverride = !string.IsNullOrEmpty(p.GetMetadata("VersionOverride"));

                // Determine the original requested version
                // if versionOverride -> get versionOverride
                // Otherwise take the version defined in CPM versions if available
                // Otherwise take the packageReference version
                string originalRequestedVersion = isVersionOverride
                    ? p.GetMetadata("VersionOverride")
                    : (versionItem != null
                    ? versionItem.GetMetadata("Version")
                    : p.GetMetadata("Version"));

                return new InstalledPackageReference(p.ItemSpec)
                {
                    OriginalRequestedVersion = originalRequestedVersion,
                    IsVersionOverride = isVersionOverride,
                };
            });

            return installedPackageReferences;

        }

        /// <summary>
        /// Returns all package references after evaluating the condition on the item groups.
        /// This method is used when we need package references for a specific target framework.
        /// </summary>
        /// <param name="project">Project for which the package references have to be obtained.</param>
        /// <param name="libraryName">Dependency of the package. If null, all references are returned</param>
        /// <param name="framework">Framework to get reference(s) for</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package references</returns>
        private static IEnumerable<ProjectItem> GetPackageReferencesPerFramework(SaveableProject project,
           string libraryName, string framework)
        {
            var globalProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
                { { "TargetFramework", framework } };
            var projectPerFramework = project.WithGlobalProperties(globalProperties).Project;

            var packages = GetPackageReferences(projectPerFramework, libraryName);
            ProjectCollection.GlobalProjectCollection.UnloadProject(projectPerFramework);

            return packages;
        }

        /// <summary>
        /// Given a project, a library dependency and a framework, it returns the package references
        /// for the specific target framework
        /// </summary>
        /// <param name="project">Project for which the package references have to be obtained.</param>
        /// <param name="libraryDependency">Dependency of the package.</param>
        /// <param name="framework">Specific framework to look at</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package references</returns>
        private static IEnumerable<ProjectItem> GetPackageReferencesPerFramework(SaveableProject project,
            LibraryDependency libraryDependency, string framework)
        {
            return GetPackageReferencesPerFramework(project, libraryDependency.Name, framework);
        }

        /// <summary>
        /// Returns all package references after evaluating the condition on the item groups.
        /// This method is used when we need package references for all target frameworks.
        /// </summary>
        /// <param name="project">Project for which the package references have to be obtained.
        /// The project should have the global property set to have a specific framework</param>
        /// <param name="libraryDependency">Dependency of the package.</param>
        /// <returns>List of Items containing the package reference for the package.
        /// If the libraryDependency is null then it returns all package reference</returns>
        private static IEnumerable<ProjectItem> GetPackageReferencesForAllFrameworks(SaveableProject project,
            LibraryDependency libraryDependency)
        {
            var frameworks = GetProjectFrameworks(project.Project);
            var mergedPackageReferences = new List<ProjectItem>();

            foreach (var framework in frameworks)
            {
                mergedPackageReferences.AddRange(GetPackageReferencesPerFramework(project, libraryDependency, framework));
            }
            return mergedPackageReferences;
        }


        private static IEnumerable<string> GetProjectFrameworks(Project project)
        {
            var frameworks = project
                .AllEvaluatedProperties
                .Where(p => p.Name.Equals(FRAMEWORK_TAG, StringComparison.OrdinalIgnoreCase))
                .Select(p => p.EvaluatedValue);

            if (!frameworks.Any())
            {
                var frameworksString = project
                    .AllEvaluatedProperties
                    .Where(p => p.Name.Equals(FRAMEWORKS_TAG, StringComparison.OrdinalIgnoreCase))
                    .Select(p => p.EvaluatedValue)
                    .FirstOrDefault();
                frameworks = MSBuildStringUtility.Split(frameworksString);
            }
            return frameworks;
        }

        private (ProjectRootElement, bool isVirtual) TryOpenProjectRootElement(string filename)
        {
            try
            {
                if (VirtualProjectBuilder?.IsValidEntryPointPath(filename) == true)
                {
                    var fullPath = Path.GetFullPath(filename);
                    var element = VirtualProjectBuilder.CreateProjectRootElement(fullPath, ProjectCollection.GlobalProjectCollection);
                    return (element, true);
                }

                // There is ProjectRootElement.TryOpen but it does not work as expected
                // I.e. it returns null for some valid projects
                return (ProjectRootElement.Open(filename, ProjectCollection.GlobalProjectCollection, preserveFormatting: true), false);
            }
            catch (Microsoft.Build.Exceptions.InvalidProjectFileException)
            {
                return (null, false);
            }
        }

        private static string GetTargetFrameworkCondition(string targetFramework)
        {
            return string.Format(CultureInfo.CurrentCulture, "'$(TargetFramework)' == '{0}'", targetFramework);
        }
    }

#nullable enable
    internal readonly struct SaveableProject
    {
        public required Project Project { get; init; }

        /// <summary>
        /// Set when this project represents a virtual project (e.g., a file-based app).
        /// </summary>
        public (string EntryPointFilePath, IVirtualProjectBuilder Builder)? VirtualProject { get; init; }

        public void Save()
        {
            if (VirtualProject is { } virtualProject)
            {
                virtualProject.Builder.SaveProject(virtualProject.EntryPointFilePath, Project.Xml);
            }
            else
            {
                Project.Save();
            }
        }

        public void Save(ProjectRootElement projectRootElement)
        {
            if (projectRootElement == Project.Xml)
            {
                Save();
            }
            else
            {
                projectRootElement.Save();
            }
        }

        public SaveableProject WithGlobalProperties(IDictionary<string, string> globalProperties)
        {
            return this with { Project = new Project(Project.Xml, globalProperties, toolsVersion: null) };
        }
    }
}