File: Shared\MSBuildProj.cs
Web Access
Project: src\src\dotnet-svcutil\lib\src\dotnet-svcutil-lib.csproj (dotnet-svcutil-lib)
// 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.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using NuGet.ProjectModel;
using Microsoft.Extensions.DependencyModel;
 
namespace Microsoft.Tools.ServiceModel.Svcutil
{
    internal class MSBuildProj : IDisposable
    {
        private const string DirBuildProps = "Directory.Build.props";
        private bool _isSaved;
        private bool _ownsDirectory;
        private readonly ProjectPropertyResolver _propertyResolver;
        private XNamespace _msbuildNS;
 
        private MSBuildProj()
        {
            _propertyResolver = new ProjectPropertyResolver();
        }
 
        #region Properties
 
        // Values in this collection have the effect of properties that are passed to MSBuild in the command line which become global properties.
        public Dictionary<string, string> GlobalProperties { get; } = new Dictionary<string, string>();
 
        // Netcore projects can specify TargetFramework and/or TargetFrameworks, TargetFramework is always used.
        // When only TargetFrameworks is specified, the build system builds the project specifying TargetFramework for each entry.
        private string _targetFramework;
        public string TargetFramework
        {
            get { return _targetFramework; }
            set { UpdateTargetFramework(value); }
        }
 
        private List<string> _targetFrameworks = new List<string>();
        private List<string> _endOfLifeTargetFrameworks = new List<string>();
        public IEnumerable<string> TargetFrameworks { get { return _targetFrameworks; } }
        internal IEnumerable<string> EndOfLifeTargetFrameworks { get { return _endOfLifeTargetFrameworks; } }
 
        private string _runtimeIdentifier;
        public string RuntimeIdentifier
        {
            get { return _runtimeIdentifier; }
            set { SetRuntimeIdentifier(value); }
        }
 
        private SortedSet<ProjectDependency> _dependencies = new SortedSet<ProjectDependency>();
        public IEnumerable<ProjectDependency> Dependencies { get { return _dependencies; } }
 
        public SortedDictionary<string, string> _resolvedProperties = new SortedDictionary<string, string>();
        public IEnumerable<KeyValuePair<string, string>> ResolvedProperties { get { return this._resolvedProperties; } }
 
        public string FileName { get; private set; }
 
        public string DirectoryPath { get; private set; }
 
        public string FullPath { get { return Path.Combine(DirectoryPath, FileName); } }
 
        public string SdkVersion { get; private set; }
 
        private XElement ProjectNode { get; set; }
 
        private XElement _projectReferenceGroup;
        private XElement ProjectReferceGroup
        {
            get
            {
                if (_projectReferenceGroup == null)
                {
                    IEnumerable<XElement> refItems = this.ProjectNode.Elements("ProjectReference");
                    if (refItems == null || refItems.Count() == 0)
                    {
                        // add ref subgroup
                        _projectReferenceGroup = new XElement("ItemGroup");
                        this.ProjectNode.Add(_projectReferenceGroup);
                    }
                    else
                    {
                        _projectReferenceGroup = refItems.FirstOrDefault().Parent;
                    }
                }
                return _projectReferenceGroup;
            }
        }
 
        private XElement _referenceGroup;
        private XElement ReferenceGroup
        {
            get
            {
                if (_referenceGroup == null)
                {
                    IEnumerable<XElement> refItems = this.ProjectNode.Elements("Reference");
                    if (refItems == null || refItems.Count() == 0)
                    {
                        // add ref subgroup
                        _referenceGroup = new XElement("ItemGroup");
                        this.ProjectNode.Add(_referenceGroup);
                    }
                    else
                    {
                        _referenceGroup = refItems.FirstOrDefault().Parent;
                    }
                }
                return _referenceGroup;
            }
        }
        #endregion
 
        private XElement _packageReferenceGroup;
        private XElement PacakgeReferenceGroup
        {
            get
            {
                if (_packageReferenceGroup == null)
                {
                    IEnumerable<XElement> refItems = this.ProjectNode.Elements("PackageReference");
                    if (refItems == null || refItems.Count() == 0)
                    {
                        // add ref subgroup
                        _packageReferenceGroup = new XElement("ItemGroup");
                        this.ProjectNode.Add(_packageReferenceGroup);
                    }
                    else
                    {
                        _packageReferenceGroup = refItems.FirstOrDefault().Parent;
                    }
                }
 
                return _packageReferenceGroup;
            }
        }
 
        #region Parsing/Settings Methods
        public static async Task<MSBuildProj> FromPathAsync(string filePath, ILogger logger, CancellationToken cancellationToken)
        {
            var project = await ParseAsync(File.ReadAllText(filePath), filePath, logger, cancellationToken).ConfigureAwait(false);
            project._isSaved = true;
            return project;
        }
 
        internal static async Task<MSBuildProj> FromPathAsync(string filePath, ILogger logger, string tfMoniker, CancellationToken cancellationToken)
        {
            var project = await ParseAsync(File.ReadAllText(filePath), filePath, logger, cancellationToken, tfMoniker).ConfigureAwait(false);
            project._isSaved = true;
            return project;
        }
 
        public static async Task<MSBuildProj> ParseAsync(string projectText, string projectFullPath, ILogger logger, CancellationToken cancellationToken, string tfMoniker = "")
        {
            using (var safeLogger = await SafeLogger.WriteStartOperationAsync(logger, $"Parsing project {Path.GetFileName(projectFullPath)}").ConfigureAwait(false))
            {
                projectFullPath = Path.GetFullPath(projectFullPath);
 
                MSBuildProj msbuildProj = new MSBuildProj
                {
                    FileName = Path.GetFileName(projectFullPath),
                    DirectoryPath = Path.GetDirectoryName(projectFullPath)
                };
 
                XDocument projDefinition = XDocument.Parse(projectText);
 
                var msbuildNS = XNamespace.None;
                if (projDefinition.Root != null && projDefinition.Root.Name != null)
                {
                    msbuildNS = projDefinition.Root.Name.Namespace;
                }
 
                msbuildProj._msbuildNS = msbuildNS;
                msbuildProj.ProjectNode = projDefinition.Element(msbuildNS + "Project");
                if (msbuildProj.ProjectNode == null)
                {
                    throw new Exception(Shared.Resources.ErrorInvalidProjectFormat);
                }
 
                // The user project can declare TargetFramework and/or TargetFrameworks property. If both are provided, TargetFramework wins.
                // When TargetFrameworks is provided, the project is built for every entry specified in the TargetFramework property.
 
                IEnumerable<XElement> targetFrameworkElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "PropertyGroup", "TargetFramework");
                if (targetFrameworkElements.Count() > 0)
                {
                    // If property is specified more than once, MSBuild will resolve it by overwriting it with the last value.
                    var targetFramework = targetFrameworkElements.Last().Value.Trim();
                    if (!string.IsNullOrWhiteSpace(targetFramework))
                    {
                        if (targetFramework.ToString().StartsWith("$"))
                        {
                            targetFramework = GetValueFromDirBuildProps(targetFramework, msbuildProj.DirectoryPath);
                        }
 
                        if (TargetFrameworkHelper.IsSupportedFramework(targetFramework, out FrameworkInfo fxInfo))
                        {
                            msbuildProj._targetFrameworks.Add(targetFramework);
                        }
                    }
                }
 
                if (msbuildProj._targetFrameworks.Count == 0)
                {
                    // TargetFramework was not provided, check TargetFrameworks property.
                    IEnumerable<XElement> targetFrameworksElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "PropertyGroup", "TargetFrameworks");
                    if (targetFrameworksElements.Count() > 0)
                    {
                        var targetFrameworks = targetFrameworksElements.Last().Value;
                        foreach (var targetFx in targetFrameworks.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()))
                        {
                            if (!string.IsNullOrEmpty(targetFx) && !targetFx.ToString().StartsWith("$"))
                            {
                                msbuildProj._targetFrameworks.Add(targetFx);
                            }
                        }
                    }
                }
 
                msbuildProj._targetFrameworks.Sort();
                msbuildProj._targetFramework = TargetFrameworkHelper.GetBestFitTargetFramework(msbuildProj._targetFrameworks);
 
                if(string.IsNullOrEmpty(msbuildProj._targetFramework))
                {
                    if(!string.IsNullOrEmpty(tfMoniker) && FrameworkInfo.TryParse(tfMoniker, out FrameworkInfo fxInfo))
                    {
                        msbuildProj._targetFramework = fxInfo.FullName;
                    }
                    else
                    {
                        msbuildProj._targetFramework = string.Concat("net", TargetFrameworkHelper.s_currentSupportedVersions.First());
                    }
 
                    msbuildProj._targetFrameworks.Add(msbuildProj._targetFramework);
                }
 
                // Ensure target framework is valid.
                FrameworkInfo frameworkInfo = TargetFrameworkHelper.GetValidFrameworkInfo(msbuildProj.TargetFramework);
 
                IEnumerable<XElement> runtimeIdentifierElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "PropertyGroup", "RuntimeIdentifier");
                if (runtimeIdentifierElements.Count() > 0)
                {
                    msbuildProj.RuntimeIdentifier = runtimeIdentifierElements.Last().Value.Trim();
                }
 
                IEnumerable<XElement> packageReferenceElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "ItemGroup", "PackageReference");
                foreach (XElement reference in packageReferenceElements)
                {
                    if(!TryGetItemIdentity(reference, out var packageName))
                        continue;
 
                    string version = GetItemValue(reference, "Version");
                    if (!ProjectDependency.IsValidVersion(version))
                    {
                        version = "";
                    }
 
                    ProjectDependency packageDep = ProjectDependency.FromPackage(packageName, version);
 
                    msbuildProj._dependencies.Add(packageDep);
                }
 
                IEnumerable<XElement> toolReferenceElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "ItemGroup", "DotNetCliToolReference");
                foreach (XElement reference in toolReferenceElements)
                {
                    if (!TryGetItemIdentity(reference, out var packageName))
                        continue;
 
                    string version = GetItemValue(reference, "Version");
 
                    ProjectDependency packageDep = ProjectDependency.FromCliTool(packageName, version);
 
                    msbuildProj._dependencies.Add(packageDep);
                }
 
                IEnumerable<XElement> projectReferenceElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "ItemGroup", "ProjectReference");
                foreach (XElement reference in projectReferenceElements)
                {
                    string projectPath = GetItemValue(reference, "Include", throwIfMissing: true);
 
                    ProjectDependency projectDep = ProjectDependency.FromProject(projectPath);
 
                    msbuildProj._dependencies.Add(projectDep);
                }
 
                IEnumerable<XElement> binReferenceElements = GetSubGroupValues(msbuildProj.ProjectNode, msbuildNS, "ItemGroup", "Reference");
                foreach (XElement reference in binReferenceElements)
                {
                    //Find hint path or path
                    string binReference = GetItemIdentity(reference);
 
                    if (!Path.IsPathRooted(binReference))
                    {
                        string fullPath = null;
                        bool fullPathFound = true;
 
                        XElement hintPath = reference.Element("HintPath");
                        XElement path = reference.Element("Path");
                        if (path != null)
                        {
                            fullPath = path.Value;
                        }
                        else if (hintPath != null)
                        {
                            fullPath = hintPath.Value;
                        }
                        else
                        {
                            try
                            {
                                fullPath = new FileInfo(Path.Combine(msbuildProj.DirectoryPath, binReference)).FullName;
                            }
                            catch
                            {
                            }
 
                            if (fullPath == null || !File.Exists(fullPath))
                            {
                                fullPathFound = false;
 
                                // If we're only targeting .NET Core or .NET Standard projects we throw if we can't find the full path to the assembly.
                                if (!TargetFrameworkHelper.ContainsFullFrameworkTarget(msbuildProj._targetFrameworks))
                                {
                                    throw new Exception(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorProjectReferenceMissingFilePathFormat, binReference));
                                }
                            }
                        }
 
                        if (fullPathFound)
                        {
                            if (System.IO.Directory.Exists(fullPath)) // IsDir?
                            {
                                fullPath = Path.Combine(fullPath, binReference);
                            }
                            else if (Directory.Exists(Path.Combine(msbuildProj.DirectoryPath, fullPath)))
                            {
                                fullPath = Path.Combine(msbuildProj.DirectoryPath, fullPath, binReference);
                            }
 
                            binReference = fullPath;
                        }
                    }
 
                    ProjectDependency projectDep = ProjectDependency.FromAssembly(binReference);
 
                    msbuildProj._dependencies.Add(projectDep);
                }
 
                // ensure we have a working directory for the ProcessRunner (ProjectPropertyResolver)..
                if (!Directory.Exists(msbuildProj.DirectoryPath))
                {
                    Directory.CreateDirectory(msbuildProj.DirectoryPath);
                    msbuildProj._ownsDirectory = true;
                }
 
                var sdkVersion = await ProjectPropertyResolver.GetSdkVersionAsync(msbuildProj.DirectoryPath, logger, cancellationToken).ConfigureAwait(false);
                msbuildProj.SdkVersion = sdkVersion ?? string.Empty;
                foreach (var tfx in msbuildProj._targetFrameworks)
                {
                    if(TargetFrameworkHelper.IsEndofLifeFramework(tfx))
                    {
                        msbuildProj._endOfLifeTargetFrameworks.Add(tfx);
                    }
                }
                return msbuildProj;
            }
        }
 
        public static async Task<MSBuildProj> DotNetNewAsync(string fullPath, ILogger logger, CancellationToken cancellationToken, string optional = "")
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            MSBuildProj project = null;
            bool ownsDir = false;
 
            if (fullPath == null)
            {
                throw new ArgumentNullException(nameof(fullPath));
            }
            fullPath = Path.GetFullPath(fullPath);
 
            string projectName = Path.GetFileNameWithoutExtension(fullPath);
            string projectExtension = Path.GetExtension(fullPath);
            string projectDir = Path.GetDirectoryName(fullPath);
 
            if (string.IsNullOrEmpty(projectName) || string.CompareOrdinal(projectExtension.ToLowerInvariant(), ".csproj") != 0)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorFullPathNotAProjectFilePathFormat, fullPath));
            }
 
            if (File.Exists(fullPath))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Shared.Resources.ErrorProjectFileAlreadyExistsFormat, fullPath));
            }
 
            // ensure we have a working directory for the ProcessRunner (ProjectPropertyResolver).
            if (!Directory.Exists(projectDir))
            {
                Directory.CreateDirectory(projectDir);
                ownsDir = true;
            }
 
            var sdkVersion = await ProjectPropertyResolver.GetSdkVersionAsync(projectDir, logger, cancellationToken).ConfigureAwait(false);
            var dotnetNewParams = $"new console {GetNoRestoreParam(sdkVersion)} --force --type project --language C# --output . --name {projectName} {optional}";
            await ProcessRunner.RunAsync("dotnet", dotnetNewParams, projectDir, logger, cancellationToken).ConfigureAwait(false);
 
            project = await ParseAsync(File.ReadAllText(fullPath), fullPath, logger, cancellationToken).ConfigureAwait(false);
            project._ownsDirectory = ownsDir;
 
            project.SdkVersion = sdkVersion ?? string.Empty;
            project._isSaved = true;
 
            return project;
        }
 
        private static string GetValueFromDirBuildProps(string elementStr, string dirPath)
        {
            try
            {
                //elementStr format: $(ElementName)
                elementStr = elementStr.Substring(2).TrimEnd(')');
                string filePath = Path.Combine(dirPath, DirBuildProps);
                XDocument doc = XDocument.Load(filePath);
                var ele = doc.Root?.Descendants(elementStr).FirstOrDefault();
                if (ele != null)
                {
                    return ele.Value;
                }
            }
            catch
            {
            }
 
            return "";
        }
 
        private static IEnumerable<XElement> GetGroupValues(XElement projectElement, string group, bool createOnMissing = false)
        {
            // XElement.Elements() always returns a collection, no need to check for null.
            IEnumerable<XElement> groups = projectElement.Elements(group);
 
            if (createOnMissing && groups.Count() == 0)
            {
                // add a property group
                XElement propertyGroup = new XElement(group);
                projectElement.Add(propertyGroup);
                return new XElement[] { propertyGroup };
            }
 
            return groups;
        }
 
        //Used for both references and properties
        private static IEnumerable<XElement> GetSubGroupValues(XElement projectElement, XNamespace msbuildNS, string group, string subGroupName)
        {
            IEnumerable<XElement> groups = GetGroupValues(projectElement, group);
            IEnumerable<XElement> subGroupValues = groups.Elements(msbuildNS + subGroupName);
            return subGroupValues;
        }
 
        private static string GetItemValue(XElement reference, string itemName, bool throwIfMissing = false)
        {
            // XElement.Attributes() alwasy returns a collection, no need to check for null.
            var itemAttribue = reference.Attributes().FirstOrDefault(item => item.Name == itemName);
            if (itemAttribue != null)
            {
                return itemAttribue.Value;
            }
 
            XElement itemNameElement = null;
            itemNameElement = reference.Elements().FirstOrDefault(item => item.Name == itemName);
            if (itemNameElement != null)
            {
                return itemNameElement.Value;
            }
 
            if (throwIfMissing)
            {
                throw new Exception(Shared.Resources.ErrorInvalidProjectFormat);
            }
            return null;
        }
 
        public static bool TryGetItemIdentity(XElement itemName, out string itemIdentity)
        {
            var itemAttribute = itemName.Attributes().FirstOrDefault(item => item.Name == "Include");
 
            if (itemAttribute != null)
            {
                itemIdentity = itemAttribute.Value;
                return true;
            }
 
            itemIdentity = default;
            return false;
        }
 
        private static string GetItemIdentity(XElement itemName)
        {
            var itemAttribute = itemName.Attributes().FirstOrDefault(item => item.Name == "Include");
            if (itemAttribute != null)
            {
                return itemAttribute.Value;
            }
 
            throw new Exception(Shared.Resources.ErrorInvalidProjectFormat);
        }
 
        public bool AddDependency(ProjectDependency dependency, bool copyInternalAssets = false)
        {
            // a nuget package can contain multiple assemblies, we need to filter package references so we don't add dups.
            bool addDependency = !_dependencies.Any(d =>
            {
                switch (d.DependencyType)
                {
                    case ProjectDependencyType.Package:
                    case ProjectDependencyType.Tool:
                        return d.Name == dependency.Name;
                    default:
                        if (d.FullPath == null && dependency.FullPath == null)
                        {
                            goto case ProjectDependencyType.Package;
                        }
                        return d.FullPath == dependency.FullPath;
                }
            });
 
            if (addDependency)
            {
                switch (dependency.DependencyType)
                {
                    case ProjectDependencyType.Project:
                        this.ProjectReferceGroup.Add(new XElement("ProjectReference", new XAttribute("Include", dependency.FullPath)));
                        break;
                    case ProjectDependencyType.Binary:
                        this.ReferenceGroup.Add(new XElement("Reference", new XAttribute("Include", dependency.AssemblyName), new XElement("HintPath", dependency.FullPath)));
                        break;
                    case ProjectDependencyType.Package:
                        this.PacakgeReferenceGroup.Add(new XElement("PackageReference", new XAttribute("Include", dependency.Name), new XAttribute("Version", dependency.Version)));
                        break;
                    case ProjectDependencyType.Tool:
                        this.ReferenceGroup.Add(new XElement("DotNetCliToolReference", new XAttribute("Include", dependency.Name), new XAttribute("Version", dependency.Version)));
                        break;
                }
 
                if(copyInternalAssets && dependency.AssemblyName == "dotnet-svcutil-lib")
                {
                    string basePath;
                    string[] frameworks = { "net6.0", "net8.0", "net462" };
                    switch (dependency.DependencyType)
                    {
                        case ProjectDependencyType.Binary:
                            basePath = dependency.FullPath.Substring(0, dependency.FullPath.LastIndexOf(Path.DirectorySeparatorChar));
                            foreach (var framework in frameworks)
                            {
                                this.ReferenceGroup.Add(new XElement("Content",
                                    new XAttribute("CopyToOutputDirectory", "always"),
                                    new XAttribute("Include", Path.Combine(basePath, $"{framework}\\**")),
                                    new XAttribute("Link", $"{framework}/%(RecursiveDir)%(Filename)%(Extension)")));
                            }
                            break;
                        case ProjectDependencyType.Package:
                            basePath = $"$(NuGetPackageRoot){dependency.Name}\\{dependency.Version}";
                            foreach (var framework in frameworks)
                            {
                                this.PacakgeReferenceGroup.Add(new XElement("Content",
                                    new XAttribute("CopyToOutputDirectory", "always"),
                                    new XAttribute("Include", $"{basePath}\\{framework}\\**"),
                                    new XAttribute("Link", $"{framework}/%(RecursiveDir)%(Filename)%(Extension)")));
                            }
                            break;
                    }
                }
 
                _dependencies.Add(dependency);
                _isSaved = false;
            }
 
            return addDependency;
        }
 
        public void SetEnableMsixTooling()
        {
            // workaround for https://github.com/microsoft/WindowsAppSDK/issues/3548: dotnet build fails when WindowsAppSDK is referenced in console application.
            // affects MAUI project targeting net7.0-windows10.0xxx, not reproduce in net8.0-window10.0xxx
            // ref: https://github.com/dotnet/maui/issues/5886
            SetPropertyValue("EnableMsixTooling", "true");
        }
 
        // Sets the property value in a PropertyGroup. Returns true if the value was changed, and false if it was already set to that value.
        private bool SetPropertyValue(string propertyName, string value)
        {
            XElement element = new XElement(propertyName, null);
 
            IEnumerable<XElement> existingElements = GetSubGroupValues(this.ProjectNode, _msbuildNS, "PropertyGroup", propertyName);
            if (existingElements.Count() > 0)
            {
                element = existingElements.Last();
            }
            else
            {
                IEnumerable<XElement> propertyGroupItems = GetGroupValues(this.ProjectNode, "PropertyGroup", createOnMissing: true);
                XElement propertyGroup = propertyGroupItems.First();
                propertyGroup.Add(element);
            }
 
            if (element.Value != value)
            {
                element.SetValue(value);
                return true;
            }
 
            return false;
        }
 
        private void SetRuntimeIdentifier(string runtimeIdentifier)
        {
            if (this.RuntimeIdentifier != runtimeIdentifier && !string.IsNullOrWhiteSpace(runtimeIdentifier))
            {
                if (SetPropertyValue("RuntimeIdentifier", runtimeIdentifier))
                {
                    _runtimeIdentifier = runtimeIdentifier;
                    _isSaved = false;
                }
            }
        }
 
        public void ClearWarningsAsErrors()
        {
            // Add an empty WarningsAsErrors element to clear the list, and treat them as warnings.
            SetPropertyValue("WarningsAsErrors", string.Empty);
        }
 
        private void UpdateTargetFramework(string targetFramework)
        {
            if (_targetFramework != targetFramework && !string.IsNullOrWhiteSpace(targetFramework))
            {
                // validate framework
                TargetFrameworkHelper.GetValidFrameworkInfo(targetFramework);
 
                if (!_targetFrameworks.Contains(targetFramework))
                {
                    // replace values (if existing).
                    if (_targetFramework != null && _targetFrameworks.Contains(_targetFramework))
                    {
                        _targetFrameworks.Remove(_targetFramework);
                    }
 
                    _targetFrameworks.Add(targetFramework);
                }
 
                IEnumerable<XElement> targetFrameworkElements = GetSubGroupValues(this.ProjectNode, _msbuildNS, "PropertyGroup", "TargetFramework");
                if (targetFrameworkElements.Count() > 0)
                {
                    var targetFrameworkNode = targetFrameworkElements.Last();
                    targetFrameworkNode.SetValue(targetFramework);
                }
 
                // TargetFramework was not provided, check TargetFrameworks property.
                IEnumerable<XElement> targetFrameworksElements = GetSubGroupValues(this.ProjectNode, _msbuildNS, "PropertyGroup", "TargetFrameworks");
                if (targetFrameworksElements.Count() > 0)
                {
                    var targetFrameworksNode = targetFrameworksElements.Last();
                    targetFrameworksNode.SetValue(_targetFrameworks.Aggregate((tfs, tf) => $"{tfs};{tf}"));
                }
 
                _targetFramework = targetFramework;
                _isSaved = false;
            }
        }
        #endregion
 
        #region Operation Methods
        public async Task SaveAsync(bool force, ILogger logger, CancellationToken cancellationToken)
        {
            _isSaved &= !force;
            await SaveAsync(logger, cancellationToken).ConfigureAwait(false);
        }
 
        public async Task SaveAsync(ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            cancellationToken.ThrowIfCancellationRequested();
 
            if (!_isSaved)
            {
                using (await SafeLogger.WriteStartOperationAsync(logger, $"Saving project file: \"{this.FullPath}\"").ConfigureAwait(false))
                {
                    if (!Directory.Exists(this.DirectoryPath))
                    {
                        Directory.CreateDirectory(this.DirectoryPath);
                        _ownsDirectory = true;
                    }
 
                    using (StreamWriter writer = File.CreateText(this.FullPath))
                    {
                        await AsyncHelper.RunAsync(() => ProjectNode.Save(writer), cancellationToken).ConfigureAwait(false);
                    }
 
                    _isSaved = true;
                }
            }
        }
 
        public async Task<ProcessRunner.ProcessResult> RestoreAsync(ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            if (!_isSaved)
            {
                await this.SaveAsync(logger, cancellationToken).ConfigureAwait(false);
            }
 
            var restoreParams = "restore --ignore-failed-sources" + (string.IsNullOrWhiteSpace(this.RuntimeIdentifier) ? "" : (" -r " + this.RuntimeIdentifier));
            // Restore no-dependencies first to workaround NuGet issue https://github.com/NuGet/Home/issues/4979
            await ProcessRunner.TryRunAsync("dotnet", restoreParams + " --no-dependencies", this.DirectoryPath, logger, cancellationToken).ConfigureAwait(false);
            var result = await ProcessRunner.TryRunAsync("dotnet", restoreParams, this.DirectoryPath, logger, cancellationToken).ConfigureAwait(false);
            return result;
        }
 
        /// <summary>
        /// Builds the project and optionally restores it before building. If restore is false the project is not saved automatically.
        /// </summary>
        /// <returns></returns>
        public async Task<ProcessRunner.ProcessResult> BuildAsync(bool restore, ILogger logger, CancellationToken cancellationToken)
        {
            if (restore)
            {
                await this.RestoreAsync(logger, cancellationToken).ConfigureAwait(false);
            }
            return await BuildAsync(logger, cancellationToken).ConfigureAwait(false);
        }
 
        public async Task<ProcessRunner.ProcessResult> BuildAsync(ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            string buildParams = $"build {GetNoRestoreParam(this.SdkVersion)}";
            return await ProcessRunner.RunAsync("dotnet", buildParams, this.DirectoryPath, logger, cancellationToken).ConfigureAwait(false);
        }
        #endregion
 
        #region Helper Methods
        public async Task<IEnumerable<ProjectDependency>> ResolveProjectReferencesAsync(IEnumerable<ProjectDependency> excludeDependencies, ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            IEnumerable<ProjectDependency> dependencies = null;
 
            if (excludeDependencies == null)
            {
                excludeDependencies = new List<ProjectDependency>();
            }
 
            using (var safeLogger = await SafeLogger.WriteStartOperationAsync(logger, "Resolving project references ...").ConfigureAwait(false))
            {
                if (_targetFrameworks.Count >= 1 && TargetFrameworkHelper.IsSupportedFramework(this.TargetFramework, out var frameworkInfo) && frameworkInfo.IsDnx)
                {
                    await this.RestoreAsync(logger, cancellationToken).ConfigureAwait(false);
 
                    var packageReferences = await ResolvePackageReferencesAsync(logger, cancellationToken).ConfigureAwait(false);
                    var assemblyReferences = await ResolveAssemblyReferencesAsync(logger, cancellationToken).ConfigureAwait(false);
                    dependencies = packageReferences.Union(assemblyReferences).Except(excludeDependencies);
                }
                else
                {
                    await safeLogger.WriteWarningAsync(Shared.Resources.WarningMultiFxOrNoSupportedDnxVersion, logToUI: true).ConfigureAwait(false);
                    dependencies = new List<ProjectDependency>();
                }
 
                await safeLogger.WriteMessageAsync($"Resolved project reference count: {dependencies.Count()}", logToUI: false).ConfigureAwait(false);
            }
 
            return dependencies;
        }
 
        private async Task<List<ProjectDependency>> ResolvePackageReferencesAsync(ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            cancellationToken.ThrowIfCancellationRequested();
 
            var packageDependencies = new List<ProjectDependency>();
 
            using (var safeLogger = await SafeLogger.WriteStartOperationAsync(logger, "Resolving package references ...").ConfigureAwait(false))
            {
                await AsyncHelper.RunAsync(async () =>
                {
                    try
                    {
                        var assetsFile = new FileInfo(Path.Combine(this.DirectoryPath, "obj", "project.assets.json")).FullName;
                        if (File.Exists(assetsFile) && !(this.TargetFramework.Contains("-") && !this.TargetFramework.ToLower().Contains("windows")))
                        {
                            LockFile lockFile = LockFileUtilities.GetLockFile(assetsFile, logger as NuGet.Common.ILogger);
                            if (lockFile != null)
                            {
                                LockFileTarget target = lockFile.Targets.Count == 1 ? lockFile.Targets[0] : lockFile.Targets.FirstOrDefault(t =>
                                t.Name.StartsWith(this.TargetFramework, StringComparison.InvariantCultureIgnoreCase) //this.TargetFramework:net7.0-windows, targets:net7.0-windows7.0
                                || this.TargetFramework.StartsWith(t.Name, StringComparison.InvariantCultureIgnoreCase));//this.TargetFramework:net7.0-windows10.0.19041.0, targets:net7.0-windows10.0.19041
                                if (target != null)
                                {
                                    foreach (var lib in target.Libraries)
                                    {
                                        bool isPackage = StringComparer.OrdinalIgnoreCase.Compare(lib.Type, "package") == 0;
 
                                        if (isPackage)
                                        {
                                            foreach (var compiletimeAssembly in lib.CompileTimeAssemblies)
                                            {
                                                if (Path.GetExtension(compiletimeAssembly.Path) == ".dll")
                                                {
                                                    var dependency = ProjectDependency.FromPackage(Path.GetFileNameWithoutExtension(compiletimeAssembly.Path), lib.Name, lib.Version.ToNormalizedString());
                                                    var itemIdx = packageDependencies.IndexOf(dependency);
 
                                                    if (itemIdx == -1)
                                                    {
                                                        packageDependencies.Add(dependency);
                                                    }
                                                    else if (dependency.IsFramework)
                                                    {
                                                        // packages can be described individually and/or as part of a platform metapackage in the lock file; for instance: Microsoft.CSharp is a package that is part of Microsoft.NetCore.
                                                        packageDependencies[itemIdx] = dependency;
                                                    }
                                                }
                                            }
                                        }
                                    }
 
                                    packageDependencies.Sort();
                                }
                                else
                                {
                                    await safeLogger.WriteWarningAsync(Shared.Resources.WarningMultiFxOrNoSupportedDnxVersion, logToUI: true).ConfigureAwait(false);
                                }
                            }
                            else
                            {
                                await safeLogger.WriteWarningAsync(Shared.Resources.WarningCannotResolveProjectReferences, logToUI: true).ConfigureAwait(false);
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        if (Utils.IsFatalOrUnexpected(ex)) throw;
                        await safeLogger.WriteWarningAsync(ex.Message, logToUI: false).ConfigureAwait(false);
                    }
                }, cancellationToken).ConfigureAwait(false);
 
                await safeLogger.WriteMessageAsync($"Package reference count: {packageDependencies.Count}", logToUI: false).ConfigureAwait(false);
            }
 
            return packageDependencies;
        }
 
        private async Task<List<ProjectDependency>> ResolveAssemblyReferencesAsync(ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            cancellationToken.ThrowIfCancellationRequested();
 
            var assemblyDependencies = new List<ProjectDependency>();
 
            using (var safeLogger = await SafeLogger.WriteStartOperationAsync(logger, $"Resolving assembly references for {this.TargetFramework} target framework ...").ConfigureAwait(false))
            {
                await ResolveProperyValuesAsync(new string[] { "OutputPath", "TargetPath" }, logger, cancellationToken).ConfigureAwait(false);
 
                var outputPath = this._resolvedProperties["OutputPath"];
                if (!Path.IsPathRooted(outputPath))
                {
                    outputPath = Path.Combine(this.DirectoryPath, outputPath.Trim(new char[] { '\"' }));
                }
 
                var depsFile = this.GlobalProperties.TryGetValue("Configuration", out var activeConfiguration) && !string.IsNullOrWhiteSpace(activeConfiguration) ?
                    Path.Combine(outputPath, $"{Path.GetFileNameWithoutExtension(this.FileName)}.deps.json") :
                    await ResolveDepsFilePathFromBuildConfigAsync(outputPath, logger, cancellationToken).ConfigureAwait(false);
 
                if (File.Exists(depsFile))
                {
                    await AsyncHelper.RunAsync(async () =>
                    {
                        try
                        {
                            DependencyContext depContext = null;
                            using (var stream = File.OpenRead(depsFile))
                            {
                                depContext = new DependencyContextJsonReader().Read(stream);
                            }
 
                            var targetLib = Path.GetFileName(this._resolvedProperties["TargetPath"].Trim('\"'));
                            if (string.IsNullOrEmpty(targetLib))
                            {
                                targetLib = $"{Path.ChangeExtension(this.FileName, ".dll")}";
                            }
 
                            foreach (var rtLib in depContext.RuntimeLibraries.Where(l => l.NativeLibraryGroups.Count == 0))
                            {
                                ProjectDependency dependency = null;
                                switch (rtLib.Type)
                                {
                                    case "project":
                                    case "reference":
                                        foreach (var assemblyGroup in rtLib.RuntimeAssemblyGroups)
                                        {
                                            foreach (var assetPath in assemblyGroup.AssetPaths)
                                            {
                                                if (!Path.GetFileName(assetPath).Equals(targetLib, RuntimeEnvironmentHelper.FileStringComparison))
                                                {
                                                    dependency = ProjectDependency.FromAssembly(Path.Combine(outputPath, assetPath));
                                                    if (File.Exists(dependency.FullPath) && !assemblyDependencies.Contains(dependency))
                                                    {
                                                        assemblyDependencies.Add(dependency);
                                                    }
                                                }
                                            }
                                        }
                                        break;
                                    //case "package":
                                    default:
                                        break;
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            if (Utils.IsFatalOrUnexpected(ex)) throw;
                            await safeLogger.WriteWarningAsync(ex.Message, logToUI: false).ConfigureAwait(false);
                        }
                    }, cancellationToken).ConfigureAwait(false);
 
                    assemblyDependencies.Sort();
                }
                else
                {
                    await safeLogger.WriteWarningAsync("Deps file not found (project not built), unable to resolve assembly/project dependencies!", logToUI: false).ConfigureAwait(false);
                }
 
                await safeLogger.WriteMessageAsync($"Assembly reference count: {assemblyDependencies.Count}", logToUI: false).ConfigureAwait(false);
            }
 
            return assemblyDependencies;
        }
 
        public async Task<IEnumerable<KeyValuePair<string, string>>> ResolveProperyValuesAsync(IEnumerable<string> propertyNames, ILogger logger, CancellationToken cancellationToken)
        {
            ThrowOnDisposed();
 
            cancellationToken.ThrowIfCancellationRequested();
 
            if (propertyNames == null)
            {
                throw new ArgumentNullException(nameof(propertyNames));
            }
 
            if (!this.GlobalProperties.Any(p => p.Key == "TargetFramework"))
            {
                this.GlobalProperties["TargetFramework"] = this.TargetFrameworks.FirstOrDefault();
            }
 
            if (!this.GlobalProperties.Any(p => p.Key == "SdkVersion"))
            {
                this.GlobalProperties["SdkVersion"] = this.SdkVersion;
            }
 
            if (!propertyNames.All(p => this._resolvedProperties.Keys.Contains(p)))
            {
                var propertyTable = this._resolvedProperties.Where(p => propertyNames.Contains(p.Key));
 
                if (propertyTable.Count() != propertyNames.Count())
                {
                    propertyTable = await _propertyResolver.EvaluateProjectPropertiesAsync(this.FullPath, this.TargetFramework, propertyNames, this.GlobalProperties, logger, cancellationToken).ConfigureAwait(false);
 
                    foreach (var entry in propertyTable)
                    {
                        this._resolvedProperties[entry.Key] = entry.Value;
                    }
                }
            }
 
            return this._resolvedProperties.Where(p => propertyNames.Contains(p.Key));
        }
 
        private async Task<string> ResolveDepsFilePathFromBuildConfigAsync(string outputPath, ILogger logger, CancellationToken cancellationToken)
        {
            // Since we are resolving the deps file path it means the passed in outputPath is built using the default build 
            // configuration (debug/release). We need to resolve the configuration by looking at the most recent build in the
            // output path. The output should look something like 'bin\Debug\netcoreapp1.0\HelloSvcutil.deps.json'
 
            using (var safeLogger = await SafeLogger.WriteStartOperationAsync(logger, $"Resolving deps.json file ...").ConfigureAwait(false))
            {
                var fileName = $"{Path.GetFileNameWithoutExtension(this.FileName)}.deps.json";
                var depsFile = string.Empty;
 
                // find the most recent deps.json files under the project's bin folder built for the project's target framework.
                var binFolder = await PathHelper.TryFindFolderAsync("bin", outputPath, logger, cancellationToken).ConfigureAwait(false);
 
                if (Directory.Exists(binFolder))
                {
                    var depsFiles = Directory.GetFiles(binFolder, "*", SearchOption.AllDirectories)
                        .Where(d => Path.GetFileName(d).Equals(fileName, RuntimeEnvironmentHelper.FileStringComparison))
                        .Where(f => PathHelper.GetFolderName(Path.GetDirectoryName(f)) == this.TargetFramework || Directory.GetParent(Directory.GetParent(f).FullName).Name == this.TargetFramework)
                        .Select(f => new FileInfo(f))
                        .OrderByDescending(f => f.CreationTimeUtc);
 
                    depsFile = depsFiles.FirstOrDefault()?.FullName;
                }
 
                await safeLogger.WriteMessageAsync($"deps file: {depsFile}", logToUI: false).ConfigureAwait(false);
                return depsFile;
            }
        }
 
        public override string ToString()
        {
            return this.FullPath;
        }
 
        private void ThrowOnDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(nameof(MSBuildProj));
            }
        }
 
        private static string GetNoRestoreParam(string sdkVersion)
        {
            if (string.IsNullOrEmpty(sdkVersion) || sdkVersion.StartsWith("1", StringComparison.OrdinalIgnoreCase))
            {
                return string.Empty;
            }
            return "--no-restore";
        }
        #endregion
 
        #region IDisposable Support
        private bool _disposed = false;
 
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    try
                    {
                        if (_ownsDirectory && Directory.Exists(this.DirectoryPath) && !DebugUtils.KeepTemporaryDirs)
                        {
                            try { Directory.Delete(this.DirectoryPath, recursive: true); } catch { }
                        }
                    }
                    catch
                    {
                    }
                }
 
                _disposed = true;
            }
        }
 
        public void Dispose()
        {
            Dispose(true);
        }
        #endregion
    }
}