File: Construction\ProjectRootElement.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
 
using Microsoft.Build.Collections;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Eventing;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
 
#nullable disable
 
namespace Microsoft.Build.Construction
{
    /// <summary>
    /// Event handler for the event fired after this project file is named or renamed.
    /// If the project file has not previously had a name, oldFullPath is null.
    /// </summary>
    internal delegate void RenameHandlerDelegate(string oldFullPath);
 
    /// <summary>
    /// ProjectRootElement class represents an MSBuild project, an MSBuild targets file or any other file that conforms to MSBuild
    /// project file schema.
    /// This class and its related classes allow a complete MSBuild project or targets file to be read and written.
    /// Comments and whitespace cannot be edited through this model at present.
    ///
    /// Each project root element is associated with exactly one ProjectCollection. This allows the owner of that project collection
    /// to control its lifetime and not be surprised by edits via another project collection.
    /// </summary>
    [DebuggerDisplay("{FullPath} #Children={Count} DefaultTargets={DefaultTargets} ToolsVersion={ToolsVersion} InitialTargets={InitialTargets} ExplicitlyLoaded={IsExplicitlyLoaded}")]
    public class ProjectRootElement : ProjectElementContainer
    {
        // Constants for default (empty) project file.
        private const string EmptyProjectFileContent = "{0}<Project{1}{2}>\r\n</Project>";
        private const string EmptyProjectFileXmlDeclaration = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
        private const string EmptyProjectFileToolsVersion = " ToolsVersion=\"" + MSBuildConstants.CurrentToolsVersion + "\"";
        internal const string EmptyProjectFileXmlNamespace = " xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\"";
 
        /// <summary>
        /// The singleton delegate that loads projects into the ProjectRootElement
        /// </summary>
        private static readonly ProjectRootElementCacheBase.OpenProjectRootElement s_openLoaderDelegate = OpenLoader;
 
        private static readonly ProjectRootElementCacheBase.OpenProjectRootElement s_openLoaderPreserveFormattingDelegate = OpenLoaderPreserveFormatting;
 
        /// <summary>
        /// Used to determine if a file is an empty XML file if it ONLY contains an XML declaration like &lt;?xml version="1.0" encoding="utf-8"?&gt;.
        /// </summary>
        private static readonly Lazy<Regex> XmlDeclarationRegEx = new Lazy<Regex>(() => new Regex(@"\A\s*\<\?\s*xml.*\?\>\s*\Z"), isThreadSafe: true);
 
        /// <summary>
        /// The default encoding to use / assume for a new project.
        /// </summary>
        private static readonly Encoding s_defaultEncoding = Encoding.UTF8;
 
        /// <summary>
        /// A global counter used to ensure each project version is distinct from every other.
        /// </summary>
        /// <remarks>
        /// This number is static so that it is unique across the appdomain. That is so that a host
        /// can know when a ProjectRootElement has been unloaded (perhaps after modification) and
        /// reloaded -- the version won't reset to '0'.
        /// </remarks>
        private static int s_globalVersionCounter;
 
        private int _version;
 
        /// <summary>
        /// Version number of this object that was last saved to disk, or last loaded from disk.
        /// Used to figure whether this object is dirty for saving.
        /// Saving to or loading from a provided stream reader does not modify this value, only saving to or loading from disk.
        /// The actual value is meaningless (since the counter is shared with all projects) --
        /// it should only be compared to a stored value.
        /// Immediately after loading from disk, this has the same value as <see cref="Version">version</see>.
        /// </summary>
        private int _versionOnDisk;
 
        /// <summary>
        /// The encoding of the project that was (if applicable) loaded off disk, and that will be used to save the project.
        /// </summary>
        /// <value>Defaults to UTF8 for new projects.</value>
        private Encoding _encoding;
 
        /// <summary>
        /// XML namespace specified and used by this project file. If a namespace was not specified in the project file, this
        /// value will be string.Empty.
        /// </summary>
        internal string XmlNamespace { get; set; }
 
        /// <summary>
        /// The project file's location. It can be null if the project is not directly loaded from a file.
        /// </summary>
        private ElementLocation _projectFileLocation;
 
        /// <summary>
        /// The project file's full path, escaped.
        /// </summary>
        private string _escapedFullPath;
 
        /// <summary>
        /// The directory that the project is in.
        /// Essential for evaluating relative paths.
        /// If the project is not loaded from disk, returns the current-directory from
        /// the time the project was loaded - this is the same behavior as Whidbey/Orcas.
        /// </summary>
        private string _directory;
 
        /// <summary>
        /// The time that this object was last changed. If it hasn't
        /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>.
        /// Stored as UTC as this is faster when there are a large number of rapid edits.
        /// </summary>
        private DateTime _timeLastChangedUtc;
 
        /// <summary>
        /// The last-write-time of the file that was read, when it was read.
        /// This can be used to see whether the file has been changed on disk
        /// by an external means.
        /// </summary>
        private DateTime _lastWriteTimeWhenReadUtc;
 
        /// <summary>
        /// Reason it was last marked dirty; unlocalized, for debugging
        /// </summary>
        private string _dirtyReason = "first created project {0}";
 
        /// <summary>
        /// Parameter to be formatted into the dirty reason
        /// </summary>
        private string _dirtyParameter = String.Empty;
 
        internal ProjectRootElementLink RootLink => (ProjectRootElementLink)Link;
 
        /// <summary>
        /// External projects support
        /// </summary>
        internal ProjectRootElement(ProjectRootElementLink link)
            : base(link)
        {
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from a XmlReader.
        /// May throw InvalidProjectFileException.
        /// Leaves the project dirty, indicating there are unsaved changes.
        /// Used to create a root element for solutions loaded by the 3.5 version of the solution wrapper.
        /// </summary>
        internal ProjectRootElement(XmlReader xmlReader, ProjectRootElementCacheBase projectRootElementCache, bool isExplicitlyLoaded,
            bool preserveFormatting)
        {
            ErrorUtilities.VerifyThrowArgumentNull(xmlReader);
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache);
 
            IsExplicitlyLoaded = isExplicitlyLoaded;
            ProjectRootElementCache = projectRootElementCache;
            _directory = NativeMethodsShared.GetCurrentDirectory();
            IncrementVersion();
 
            XmlDocumentWithLocation document = LoadDocument(xmlReader, preserveFormatting);
 
            ProjectParser.Parse(document, this);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Leaves the project dirty, indicating there are unsaved changes.
        /// </summary>
        private ProjectRootElement(ProjectRootElementCacheBase projectRootElementCache, NewProjectFileOptions projectFileOptions)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache);
 
            ProjectRootElementCache = projectRootElementCache;
            _directory = NativeMethodsShared.GetCurrentDirectory();
            IncrementVersion();
 
            var document = new XmlDocumentWithLocation();
            var xrs = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore };
 
            var emptyProjectFile = string.Format(EmptyProjectFileContent,
                (projectFileOptions & NewProjectFileOptions.IncludeXmlDeclaration) != 0 ? EmptyProjectFileXmlDeclaration : string.Empty,
                (projectFileOptions & NewProjectFileOptions.IncludeToolsVersion) != 0 ? EmptyProjectFileToolsVersion : string.Empty,
                (projectFileOptions & NewProjectFileOptions.IncludeXmlNamespace) != 0 ? EmptyProjectFileXmlNamespace : string.Empty);
 
            using (XmlReader xr = XmlReader.Create(new StringReader(emptyProjectFile), xrs))
            {
                document.Load(xr);
            }
 
            ProjectParser.Parse(document, this);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance over a project with the specified file path.
        /// Assumes path is already normalized.
        /// May throw InvalidProjectFileException.
        /// </summary>
        private ProjectRootElement(
                string path,
                ProjectRootElementCacheBase projectRootElementCache,
                bool preserveFormatting)
        {
            ErrorUtilities.VerifyThrowArgumentLength(path);
            ErrorUtilities.VerifyThrowInternalRooted(path);
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache);
            ProjectRootElementCache = projectRootElementCache;
 
            IncrementVersion();
            _versionOnDisk = Version;
            _timeLastChangedUtc = DateTime.UtcNow;
 
            XmlDocumentWithLocation document = LoadDocument(path, preserveFormatting, projectRootElementCache.LoadProjectsReadOnly);
 
            ProjectParser.Parse(document, this);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an existing document.
        /// May throw InvalidProjectFileException.
        /// Leaves the project dirty, indicating there are unsaved changes.
        /// </summary>
        /// <remarks>
        /// Do not make public: we do not wish to expose particular XML API's.
        /// </remarks>
        private ProjectRootElement(XmlDocumentWithLocation document, ProjectRootElementCacheBase projectRootElementCache)
        {
            ErrorUtilities.VerifyThrowArgumentNull(document);
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElementCache);
 
            ProjectRootElementCache = projectRootElementCache;
            _directory = NativeMethodsShared.GetCurrentDirectory();
            IncrementVersion();
 
            ProjectParser.Parse(document, this);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an existing document.
        /// Helper constructor for the <see cref="ReloadFrom(string,bool,bool?)"/>> mehtod which needs to check if the document parses
        /// </summary>
        /// <remarks>
        /// Do not make public: we do not wish to expose particular XML API's.
        /// </remarks>
        private ProjectRootElement(XmlDocumentWithLocation document)
        {
            ProjectParser.Parse(document, this);
        }
 
        /// <summary>
        /// Event raised after this project is renamed
        /// </summary>
        internal event RenameHandlerDelegate OnAfterProjectRename;
 
        /// <summary>
        /// Event raised after the project XML is changed.
        /// </summary>
        internal event EventHandler<ProjectXmlChangedEventArgs> OnProjectXmlChanged;
 
        /// <summary>
        /// Condition should never be set, but the getter returns null instead of throwing
        /// because a nonexistent condition is implicitly true
        /// </summary>
        public override string Condition
        {
            get => null;
            set => ErrorUtilities.ThrowInvalidOperation("OM_CannotGetSetCondition");
        }
 
        #region ChildEnumerators
        /// <summary>
        /// Get a read-only collection of the child chooses, if any
        /// </summary>
        /// <remarks>
        /// The name is inconsistent to make it more understandable, per API review.
        /// </remarks>
        public ICollection<ProjectChooseElement> ChooseElements => GetChildrenOfType<ProjectChooseElement>();
 
        /// <summary>
        /// Get a read-only collection of the child item definition groups, if any
        /// </summary>
        public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroups => GetChildrenOfType<ProjectItemDefinitionGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child item definitions, if any, in all item definition groups anywhere in the project file.
        /// </summary>
        public ICollection<ProjectItemDefinitionElement> ItemDefinitions => new ReadOnlyCollection<ProjectItemDefinitionElement>(GetAllChildrenOfType<ProjectItemDefinitionElement>());
 
        /// <summary>
        /// Get a read-only collection over the child item groups, if any.
        /// Does not include any that may not be at the root, i.e. inside Choose elements.
        /// </summary>
        public ICollection<ProjectItemGroupElement> ItemGroups => GetChildrenOfType<ProjectItemGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child items, if any, in all item groups anywhere in the project file.
        /// Not restricted to root item groups: traverses through Choose elements.
        /// </summary>
        public ICollection<ProjectItemElement> Items => new ReadOnlyCollection<ProjectItemElement>(GetAllChildrenOfType<ProjectItemElement>());
 
        /// <summary>
        /// Get a read-only collection of the child import groups, if any.
        /// </summary>
        public ICollection<ProjectImportGroupElement> ImportGroups => GetChildrenOfType<ProjectImportGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child imports
        /// </summary>
        public ICollection<ProjectImportElement> Imports => new ReadOnlyCollection<ProjectImportElement>(GetAllChildrenOfType<ProjectImportElement>());
 
        /// <summary>
        /// Get a read-only collection of the child property groups, if any.
        /// Does not include any that may not be at the root, i.e. inside Choose elements.
        /// </summary>
        public ICollection<ProjectPropertyGroupElement> PropertyGroups => GetChildrenOfType<ProjectPropertyGroupElement>();
 
        /// <summary>
        /// Geta read-only collection of the child properties, if any, in all property groups anywhere in the project file.
        /// Not restricted to root property groups: traverses through Choose elements.
        /// </summary>
        public ICollection<ProjectPropertyElement> Properties => new ReadOnlyCollection<ProjectPropertyElement>(GetAllChildrenOfType<ProjectPropertyElement>());
 
        /// <summary>
        /// Get a read-only collection of the child targets
        /// </summary>
        public ICollection<ProjectTargetElement> Targets => GetChildrenOfType<ProjectTargetElement>();
 
        /// <summary>
        /// Get a read-only collection of the child usingtasks, if any
        /// </summary>
        public ICollection<ProjectUsingTaskElement> UsingTasks => GetChildrenOfType<ProjectUsingTaskElement>();
 
        /// <summary>
        /// Get a read-only collection of the child item groups, if any, in reverse order
        /// </summary>
        public ICollection<ProjectItemGroupElement> ItemGroupsReversed => GetChildrenReversedOfType<ProjectItemGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child item definition groups, if any, in reverse order
        /// </summary>
        public ICollection<ProjectItemDefinitionGroupElement> ItemDefinitionGroupsReversed => GetChildrenReversedOfType<ProjectItemDefinitionGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child import groups, if any, in reverse order
        /// </summary>
        public ICollection<ProjectImportGroupElement> ImportGroupsReversed => GetChildrenReversedOfType<ProjectImportGroupElement>();
 
        /// <summary>
        /// Get a read-only collection of the child property groups, if any, in reverse order
        /// </summary>
        public ICollection<ProjectPropertyGroupElement> PropertyGroupsReversed => GetChildrenReversedOfType<ProjectPropertyGroupElement>();
 
        #endregion
 
        /// <summary>
        /// The directory that the project is in.
        /// Essential for evaluating relative paths.
        /// Is never null, even if the FullPath does not contain directory information.
        /// If the project has not been loaded from disk and has not been given a path, returns the current-directory from
        /// the time the project was loaded - this is the same behavior as Whidbey/Orcas.
        /// If the project has not been loaded from disk but has been given a path, this path may not exist.
        /// </summary>
        public string DirectoryPath
        {
            get => Link != null ? RootLink.DirectoryPath : _directory ?? String.Empty;
            internal set => _directory = value;
            // Used during solution load to ensure solutions which were created from a file have a location.
        }
 
        public string EscapedFullPath => _escapedFullPath ?? (_escapedFullPath = ProjectCollection.Escape(FullPath));
 
        /// <summary>
        /// Full path to the project file.
        /// If the project has not been loaded from disk and has not been given a path, returns null.
        /// If the project has not been loaded from disk but has been given a path, this path may not exist.
        /// Setter renames the project, if it already had a name.
        /// </summary>
        /// <remarks>
        /// Updates the ProjectRootElement cache.
        /// </remarks>
        public string FullPath
        {
            get => Link != null ? RootLink.FullPath : _projectFileLocation?.File;
 
            set
            {
                ErrorUtilities.VerifyThrowArgumentLength(value);
                if (Link != null)
                {
                    RootLink.FullPath = value;
                    return;
                }
 
                string oldFullPath = _projectFileLocation?.File;
 
                // We do not control the current directory at this point, but assume that if we were
                // passed a relative path, the caller assumes we will prepend the current directory.
                string newFullPath = FileUtilities.NormalizePath(value);
 
                if (String.Equals(oldFullPath, newFullPath, StringComparison.OrdinalIgnoreCase))
                {
                    return;
                }
 
                _projectFileLocation = ElementLocation.Create(newFullPath);
                _escapedFullPath = null;
                _directory = Path.GetDirectoryName(newFullPath);
 
                if (XmlDocument != null)
                {
                    XmlDocument.FullPath = newFullPath;
                }
 
                if (oldFullPath == null)
                {
                    ProjectRootElementCache.AddEntry(this);
                }
                else
                {
                    ProjectRootElementCache.RenameEntry(oldFullPath, this);
                }
 
                OnAfterProjectRename?.Invoke(oldFullPath);
 
                MarkDirty("Set project FullPath to '{0}'", FullPath);
            }
        }
 
        /// <summary>
        /// Encoding that the project file is saved in, or will be saved in, unless
        /// otherwise specified.
        /// </summary>
        /// <remarks>
        /// Returns the encoding from the Xml declaration if any, otherwise UTF8.
        /// </remarks>
        public Encoding Encoding
        {
            get
            {
                if (Link != null)
                {
                    return RootLink.Encoding;
                }
 
                // No thread-safety lock required here because many reader threads would set the same value to the field.
                if (_encoding == null)
                {
                    var declaration = XmlDocument.FirstChild as XmlDeclaration;
 
                    if (declaration?.Encoding.Length > 0)
                    {
                        _encoding = Encoding.GetEncoding(declaration.Encoding);
                    }
                }
 
                // Ensure we never return null, in case there was no xml declaration that we could find above.
                return _encoding ?? s_defaultEncoding;
            }
        }
 
        /// <summary>
        /// Gets or sets the value of DefaultTargets. If there is no DefaultTargets, returns empty string.
        /// If the value is null or empty, removes the attribute.
        /// </summary>
        public string DefaultTargets
        {
            [DebuggerStepThrough]
            get => GetAttributeValue(XMakeAttributes.defaultTargets);
 
            [DebuggerStepThrough]
            set => SetOrRemoveAttribute(XMakeAttributes.defaultTargets, value, "Set Project DefaultTargets to '{0}'", value);
        }
 
        /// <summary>
        /// Gets or sets the value of InitialTargets. If there is no InitialTargets, returns empty string.
        /// If the value is null or empty, removes the attribute.
        /// </summary>
        public string InitialTargets
        {
            [DebuggerStepThrough]
            get => GetAttributeValue(XMakeAttributes.initialTargets);
 
            [DebuggerStepThrough]
            set => SetOrRemoveAttribute(XMakeAttributes.initialTargets, value, "Set project InitialTargets to '{0}'", value);
        }
 
        /// <summary>
        /// Gets or sets a semicolon delimited list of software development kits (SDK) that the project uses.
        /// If  a value is specified, an Sdk.props is simplicity imported at the top of the project and an
        /// Sdk.targets is simplicity imported at the bottom from the specified SDK.
        /// If the value is null or empty, removes the attribute.
        /// </summary>
        public string Sdk
        {
            [DebuggerStepThrough]
            get => GetAttributeValue(XMakeAttributes.sdk);
 
            [DebuggerStepThrough]
            set => SetOrRemoveAttribute(XMakeAttributes.sdk, value, "Set project Sdk to '{0}'", value);
        }
 
        /// <summary>
        /// Gets or sets the value of TreatAsLocalProperty. If there is no tag, returns empty string.
        /// If the value being set is null or empty, removes the attribute.
        /// </summary>
        public string TreatAsLocalProperty
        {
            [DebuggerStepThrough]
            get => GetAttributeValue(XMakeAttributes.treatAsLocalProperty);
 
            [DebuggerStepThrough]
            set => SetOrRemoveAttribute(XMakeAttributes.treatAsLocalProperty, value, "Set project TreatAsLocalProperty to '{0}'", value);
        }
 
        /// <summary>
        /// Gets or sets the value of ToolsVersion. If there is no ToolsVersion, returns empty string.
        /// If the value is null or empty, removes the attribute.
        /// </summary>
        public string ToolsVersion
        {
            [DebuggerStepThrough]
            get => GetAttributeValue(XMakeAttributes.toolsVersion);
 
            [DebuggerStepThrough]
            set => SetOrRemoveAttribute(XMakeAttributes.toolsVersion, value, "Set project ToolsVersion {0}", value);
        }
 
        /// <summary>
        /// Gets the XML representing this project as a string.
        /// Does not remove any dirty flag.
        /// </summary>
        /// <remarks>
        /// Useful for debugging.
        /// Note that we do not expose an XmlDocument or any other specific XML API.
        /// </remarks>
        public string RawXml
        {
            get
            {
                if (Link != null)
                {
                    return RootLink.RawXml;
                }
 
                using (var stringWriter = new EncodingStringWriter(Encoding))
                {
                    using (var projectWriter = new ProjectWriter(stringWriter))
                    {
                        projectWriter.Initialize(XmlDocument);
                        XmlDocument.Save(projectWriter);
                    }
 
                    return stringWriter.ToString();
                }
            }
        }
 
        /// <summary>
        /// Whether the XML has been modified since it was last loaded or saved.
        /// </summary>
        public bool HasUnsavedChanges => Link != null ? RootLink.HasUnsavedChanges : Version != _versionOnDisk;
 
        /// <summary>
        /// Whether the XML is preserving formatting or not.
        /// </summary>
        public bool PreserveFormatting => Link != null ? RootLink.PreserveFormatting : XmlDocument?.PreserveWhitespace ?? false;
 
        /// <summary>
        /// Version number of this object.
        /// A host can compare this to a stored version number to determine whether
        /// a project's XML has changed, even if it has also been saved since.
        ///
        /// The actual value is meaningless: an edit may increment it more than once,
        /// so it should only be compared to a stored value.
        /// </summary>
        /// <remarks>
        /// Used by the Project class to figure whether changes have occurred that
        /// it might want to pick up by reevaluation.
        ///
        /// Used by the ProjectRootElement class to determine whether it needs to save.
        ///
        /// This number is unique to the appdomain. That means that it is possible
        /// to know when a ProjectRootElement has been unloaded (perhaps after modification) and
        /// reloaded -- the version won't reset to '0'.
        ///
        /// We're assuming we don't have over 2 billion edits.
        /// </remarks>
        public int Version
        {
            get => Link != null ? RootLink.Version : _version;
            private set => _version = value;
        }
 
        /// <summary>
        /// The time that this object was last changed. If it hasn't
        /// been changed since being loaded or created, its value is <see cref="DateTime.MinValue"/>.
        /// </summary>
        /// <remarks>
        /// This is used by the VB/C# project system.
        /// </remarks>
        public DateTime TimeLastChanged => Link != null ? RootLink.TimeLastChanged : _timeLastChangedUtc.ToLocalTime();
 
        /// <summary>
        /// The last-write-time of the file that was read, when it was read.
        /// This can be used to see whether the file has been changed on disk
        /// by an external means.
        /// </summary>
        public DateTime LastWriteTimeWhenRead => Link != null ? RootLink.LastWriteTimeWhenRead : _lastWriteTimeWhenReadUtc.ToLocalTime();
 
        internal DateTime? StreamTimeUtc = null;
 
        /// <summary>
        /// This does not allow conditions, so it should not be called.
        /// </summary>
        public override ElementLocation ConditionLocation
        {
            get
            {
                ErrorUtilities.ThrowInternalError("Should not evaluate this");
                return null;
            }
        }
 
        /// <summary>
        /// Location of the originating file itself, not any specific content within it.
        /// If the file has not been given a name, returns an empty location.
        /// This is a case where it is legitimate to "not have a location".
        /// </summary>
        public ElementLocation ProjectFileLocation => Link != null ? RootLink.ProjectFileLocation : _projectFileLocation ?? ElementLocation.EmptyLocation;
 
        /// <summary>
        /// Location of the toolsversion attribute, if any
        /// </summary>
        public ElementLocation ToolsVersionLocation => GetAttributeLocation(XMakeAttributes.toolsVersion);
 
        /// <summary>
        /// Location of the defaulttargets attribute, if any
        /// </summary>
        public ElementLocation DefaultTargetsLocation => GetAttributeLocation(XMakeAttributes.defaultTargets);
 
        /// <summary>
        /// Location of the initialtargets attribute, if any
        /// </summary>
        public ElementLocation InitialTargetsLocation => GetAttributeLocation(XMakeAttributes.initialTargets);
 
        /// <summary>
        /// Location of the Sdk attribute, if any
        /// </summary>
        public ElementLocation SdkLocation => GetAttributeLocation(XMakeAttributes.sdk);
 
        /// <summary>
        /// Location of the TreatAsLocalProperty attribute, if any
        /// </summary>
        public ElementLocation TreatAsLocalPropertyLocation => GetAttributeLocation(XMakeAttributes.treatAsLocalProperty);
 
        /// <summary>
        /// Has the project root element been explicitly loaded for a build or has it been implicitly loaded
        /// as part of building another project.
        /// </summary>
        /// <remarks>
        /// Internal code that wants to set this to true should call <see cref="MarkAsExplicitlyLoaded"/>.
        /// The setter is private to make it more difficult to downgrade an existing PRE to an implicitly loaded state, which should never happen.
        /// </remarks>
        internal bool IsExplicitlyLoaded { get; private set; }
 
        /// <summary>
        /// Retrieves the root element cache with which this root element is associated.
        /// </summary>
        internal ProjectRootElementCacheBase ProjectRootElementCache { get; }
 
        /// <summary>
        /// Gets a value indicating whether this PRE is known by its containing collection.
        /// </summary>
        internal bool IsMemberOfProjectCollection => _projectFileLocation != null;
 
        /// <summary>
        /// Indicates whether there are any targets in this project
        /// that use the "Returns" attribute.  If so, then this project file
        /// is automatically assumed to be "Returns-enabled", and the default behavior
        /// for targets without Returns attributes changes from using the Outputs to
        /// returning nothing by default.
        /// </summary>
        internal bool ContainsTargetsWithReturnsAttribute { get; set; }
 
        /// <summary>
        /// Gets the ProjectExtensions child, if any, otherwise null.
        /// </summary>
        /// <remarks>
        /// Not public as we do not wish to encourage the use of ProjectExtensions.
        /// </remarks>
        internal ProjectExtensionsElement ProjectExtensions
            => GetChildrenReversedOfType<ProjectExtensionsElement>().FirstOrDefault();
 
        /// <summary>
        /// Returns an unlocalized indication of how this file was last dirtied.
        /// This is for debugging purposes only.
        /// String formatting only occurs when retrieved.
        /// </summary>
        internal string LastDirtyReason
            => _dirtyReason == null ? null : String.Format(CultureInfo.InvariantCulture, _dirtyReason, _dirtyParameter);
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the global project collection.
        /// </summary>
        public static ProjectRootElement Create()
        {
            return Create(ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="NewProjectFileOptions"/>.
        /// Uses the global project collection.
        /// </summary>
        public static ProjectRootElement Create(NewProjectFileOptions projectFileOptions)
        {
            return Create(ProjectCollection.GlobalProjectCollection, projectFileOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the specified project collection.
        /// </summary>
        public static ProjectRootElement Create(ProjectCollection projectCollection)
        {
            return Create(projectCollection.ProjectRootElementCache);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified <see cref="ProjectCollection"/> and <see cref="NewProjectFileOptions"/>.
        /// </summary>
        public static ProjectRootElement Create(ProjectCollection projectCollection, NewProjectFileOptions projectFileOptions)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
 
            return Create(projectCollection.ProjectRootElementCache, projectFileOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the global project collection.
        /// </summary>
        public static ProjectRootElement Create(string path)
        {
            return Create(path, ProjectCollection.GlobalProjectCollection, Project.DefaultNewProjectTemplateOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later using the specified path and <see cref="NewProjectFileOptions"/>.
        /// Uses the global project collection.
        /// </summary>
        public static ProjectRootElement Create(string path, NewProjectFileOptions newProjectFileOptions)
        {
            return Create(path, ProjectCollection.GlobalProjectCollection, newProjectFileOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the specified project collection.
        /// </summary>
        public static ProjectRootElement Create(string path, ProjectCollection projectCollection)
        {
            return Create(path, projectCollection, Project.DefaultNewProjectTemplateOptions);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the specified project collection.
        /// </summary>
        public static ProjectRootElement Create(string path, ProjectCollection projectCollection, NewProjectFileOptions newProjectFileOptions)
        {
            ErrorUtilities.VerifyThrowArgumentLength(path);
            ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
 
            var projectRootElement = new ProjectRootElement(
                projectCollection.ProjectRootElementCache,
                newProjectFileOptions)
            { FullPath = path };
 
            return projectRootElement;
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an XmlReader.
        /// Uses the global project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        public static ProjectRootElement Create(XmlReader xmlReader)
        {
            return Create(xmlReader, ProjectCollection.GlobalProjectCollection, preserveFormatting: false);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an XmlReader.
        /// Uses the specified project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection)
        {
            return Create(xmlReader, projectCollection, preserveFormatting: false);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an XmlReader.
        /// Uses the specified project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, bool preserveFormatting)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
 
            return new ProjectRootElement(xmlReader, projectCollection.ProjectRootElementCache, true /*Explicitly loaded*/,
                preserveFormatting);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance by loading from the specified file path.
        /// Uses the global project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        public static ProjectRootElement Open(string path)
        {
            return Open(path, ProjectCollection.GlobalProjectCollection);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance by loading from the specified file path.
        /// Uses the specified project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        public static ProjectRootElement Open(string path, ProjectCollection projectCollection)
        {
            return Open(path, projectCollection,
                preserveFormatting: null);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance by loading from the specified file path.
        /// Uses the specified project collection and preserves the formatting of the document if specified.
        /// </summary>
        public static ProjectRootElement Open(string path, ProjectCollection projectCollection, bool? preserveFormatting)
        {
            ErrorUtilities.VerifyThrowArgumentLength(path);
            ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
 
            path = FileUtilities.NormalizePath(path);
 
            return Open(path, projectCollection.ProjectRootElementCache, true /*Is explicitly loaded*/, preserveFormatting);
        }
 
        /// <summary>
        /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
        /// Uses the global project collection.
        /// </summary>
        /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
        /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
        /// <remarks>
        /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
        /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
        /// </remarks>
        public static ProjectRootElement TryOpen(string path)
        {
            ErrorUtilities.VerifyThrowArgumentLength(path);
 
            return TryOpen(path, ProjectCollection.GlobalProjectCollection);
        }
 
        /// <summary>
        /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
        /// Uses the specified project collection.
        /// </summary>
        /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
        /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param>
        /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
        /// <remarks>
        /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
        /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
        /// </remarks>
        public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection)
        {
            return TryOpen(path, projectCollection, preserveFormatting: null);
        }
 
        /// <summary>
        /// Returns the ProjectRootElement for the given path if it has been loaded, or null if it is not currently in memory.
        /// Uses the specified project collection.
        /// </summary>
        /// <param name="path">The path of the ProjectRootElement, cannot be null.</param>
        /// <param name="projectCollection">The <see cref="ProjectCollection"/> to load the project into.</param>
        /// <param name="preserveFormatting">
        /// The formatting to open with. Must match the formatting in the collection to succeed.
        /// </param>
        /// <returns>The loaded ProjectRootElement, or null if it is not currently in memory.</returns>
        /// <remarks>
        /// It is possible for ProjectRootElements to be brought into memory and discarded due to memory pressure. Therefore
        /// this method returning false does not indicate that it has never been loaded, only that it is not currently in memory.
        /// </remarks>
        public static ProjectRootElement TryOpen(string path, ProjectCollection projectCollection, bool? preserveFormatting)
        {
            ErrorUtilities.VerifyThrowArgumentLength(path);
            ErrorUtilities.VerifyThrowArgumentNull(projectCollection);
 
            path = FileUtilities.NormalizePath(path);
 
            ProjectRootElement projectRootElement = projectCollection.ProjectRootElementCache.TryGet(path, preserveFormatting);
 
            return projectRootElement;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// If import groups exist, inserts into the last one without a condition on it.
        /// Otherwise, creates an import at the end of the project.
        /// </summary>
        public ProjectImportElement AddImport(string project)
        {
            ErrorUtilities.VerifyThrowArgumentLength(project);
 
            ProjectImportGroupElement importGroupToAddTo =
                ImportGroupsReversed.FirstOrDefault(importGroup => importGroup.Condition.Length <= 0);
 
            ProjectImportElement import;
 
            if (importGroupToAddTo != null)
            {
                import = importGroupToAddTo.AddImport(project);
            }
            else
            {
                import = CreateImportElement(project);
                AppendChild(import);
            }
 
            return import;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Creates an import group at the end of the project.
        /// </summary>
        public ProjectImportGroupElement AddImportGroup()
        {
            ProjectImportGroupElement importGroup = CreateImportGroupElement();
            AppendChild(importGroup);
 
            return importGroup;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Finds item group with no condition with at least one item of same type, or else adds a new item group;
        /// adds the item to that item group with items of the same type, ordered by include.
        /// </summary>
        /// <remarks>
        /// Per the previous implementation, it actually finds the last suitable item group, not the first.
        /// </remarks>
        public ProjectItemElement AddItem(string itemType, string include)
        {
            return AddItem(itemType, include, null);
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Finds first item group with no condition with at least one item of same type, or else an empty item group; or else adds a new item group;
        /// adds the item to that item group with items of the same type, ordered by include.
        /// Does not attempt to check whether the item matches an existing wildcard expression; that is only possible
        /// in the evaluated world.
        /// </summary>
        /// <remarks>
        /// Per the previous implementation, it actually finds the last suitable item group, not the first.
        /// </remarks>
        public ProjectItemElement AddItem(string itemType, string include, IEnumerable<KeyValuePair<string, string>> metadata)
        {
            ErrorUtilities.VerifyThrowArgumentLength(itemType);
            ErrorUtilities.VerifyThrowArgumentLength(include);
 
            ProjectItemGroupElement itemGroupToAddTo = null;
 
            foreach (ProjectItemGroupElement itemGroup in ItemGroups)
            {
                if (itemGroup.Condition.Length > 0)
                {
                    continue;
                }
 
                if (itemGroupToAddTo == null && itemGroup.Count == 0)
                {
                    itemGroupToAddTo = itemGroup;
                }
 
                if (itemGroup.Items.Any(item => MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, item.ItemType)))
                {
                    itemGroupToAddTo = itemGroup;
                }
 
                if (itemGroupToAddTo?.Count > 0)
                {
                    break;
                }
            }
 
            if (itemGroupToAddTo == null)
            {
                itemGroupToAddTo = AddItemGroup();
            }
 
            ProjectItemElement newItem = itemGroupToAddTo.AddItem(itemType, include, metadata);
 
            return newItem;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Adds an item group after the last existing item group, if any; otherwise
        /// adds an item group after the last existing property group, if any; otherwise
        /// adds a new item group at the end of the project.
        /// </summary>
        public ProjectItemGroupElement AddItemGroup()
        {
            ProjectElement reference = ItemGroupsReversed.FirstOrDefault();
 
            if (reference == null)
            {
                foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
                {
                    reference = propertyGroup;
                    break;
                }
            }
 
            ProjectItemGroupElement newItemGroup = CreateItemGroupElement();
 
            if (reference == null)
            {
                AppendChild(newItemGroup);
            }
            else
            {
                InsertAfterChild(newItemGroup, reference);
            }
 
            return newItemGroup;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Finds first item definition group with no condition with at least one item definition of same item type, or else adds a new item definition group.
        /// </summary>
        public ProjectItemDefinitionElement AddItemDefinition(string itemType)
        {
            ErrorUtilities.VerifyThrowArgumentLength(itemType);
 
            ProjectItemDefinitionGroupElement itemDefinitionGroupToAddTo = null;
 
            foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroups)
            {
                if (itemDefinitionGroup.Condition.Length > 0)
                {
                    continue;
                }
 
                foreach (ProjectItemDefinitionElement itemDefinition in itemDefinitionGroup.ItemDefinitions)
                {
                    if (MSBuildNameIgnoreCaseComparer.Default.Equals(itemType, itemDefinition.ItemType))
                    {
                        itemDefinitionGroupToAddTo = itemDefinitionGroup;
                        break;
                    }
                }
 
                if (itemDefinitionGroupToAddTo != null)
                {
                    break;
                }
            }
 
            if (itemDefinitionGroupToAddTo == null)
            {
                itemDefinitionGroupToAddTo = AddItemDefinitionGroup();
            }
 
            ProjectItemDefinitionElement newItemDefinition = CreateItemDefinitionElement(itemType);
 
            itemDefinitionGroupToAddTo.AppendChild(newItemDefinition);
 
            return newItemDefinition;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Adds an item definition group after the last existing item definition group, if any; otherwise
        /// adds an item definition group after the last existing property group, if any; otherwise
        /// adds a new item definition group at the end of the project.
        /// </summary>
        public ProjectItemDefinitionGroupElement AddItemDefinitionGroup()
        {
            ProjectElement reference = null;
 
            foreach (ProjectItemDefinitionGroupElement itemDefinitionGroup in ItemDefinitionGroupsReversed)
            {
                reference = itemDefinitionGroup;
                break;
            }
 
            if (reference == null)
            {
                foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
                {
                    reference = propertyGroup;
                    break;
                }
            }
 
            ProjectItemDefinitionGroupElement newItemDefinitionGroup = CreateItemDefinitionGroupElement();
 
            InsertAfterChild(newItemDefinitionGroup, reference);
 
            return newItemDefinitionGroup;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Adds a new property group after the last existing property group, if any; otherwise
        /// at the start of the project.
        /// </summary>
        public ProjectPropertyGroupElement AddPropertyGroup()
        {
            ProjectPropertyGroupElement reference = null;
 
            foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroupsReversed)
            {
                reference = propertyGroup;
                break;
            }
 
            ProjectPropertyGroupElement newPropertyGroup = CreatePropertyGroupElement();
 
            InsertAfterChild(newPropertyGroup, reference);
 
            return newPropertyGroup;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic.
        /// Updates the last existing property with the specified name that has no condition on itself or its property group, if any.
        /// Otherwise, adds a new property in the first property group without a condition, creating a property group if necessary after
        /// the last existing property group, else at the start of the project.
        /// </summary>
        public ProjectPropertyElement AddProperty(string name, string value)
        {
            ProjectPropertyGroupElement matchingPropertyGroup = null;
            ProjectPropertyElement matchingProperty = null;
 
            foreach (ProjectPropertyGroupElement propertyGroup in PropertyGroups)
            {
                if (propertyGroup.Condition.Length > 0)
                {
                    continue;
                }
 
                if (matchingPropertyGroup == null)
                {
                    matchingPropertyGroup = propertyGroup;
                }
 
                foreach (ProjectPropertyElement property in propertyGroup.Properties)
                {
                    if (property.Condition.Length > 0)
                    {
                        continue;
                    }
 
                    if (MSBuildNameIgnoreCaseComparer.Default.Equals(property.Name, name))
                    {
                        matchingProperty = property;
                    }
                }
            }
 
            if (matchingProperty != null)
            {
                matchingProperty.Value = value;
 
                return matchingProperty;
            }
 
            if (matchingPropertyGroup == null)
            {
                matchingPropertyGroup = AddPropertyGroup();
            }
 
            ProjectPropertyElement newProperty = matchingPropertyGroup.AddProperty(name, value);
 
            return newProperty;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Creates a target at the end of the project.
        /// </summary>
        public ProjectTargetElement AddTarget(string name)
        {
            ProjectTargetElement target = CreateTargetElement(name);
            AppendChild(target);
 
            return target;
        }
 
        /// <summary>
        /// Convenience method that picks a location based on a heuristic:
        /// Creates a usingtask at the end of the project.
        /// Exactly one of assemblyName or assemblyFile must be null.
        /// </summary>
        public ProjectUsingTaskElement AddUsingTask(string name, string assemblyFile, string assemblyName)
        {
            ProjectUsingTaskElement usingTask = CreateUsingTaskElement(name, FileUtilities.FixFilePath(assemblyFile), assemblyName);
            AppendChild(usingTask);
 
            return usingTask;
        }
 
        /// <summary>
        /// Creates a choose.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectChooseElement CreateChooseElement()
        {
            return Link != null ? RootLink.CreateChooseElement() : ProjectChooseElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates an import.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectImportElement CreateImportElement(string project)
        {
            return Link != null ? RootLink.CreateImportElement(project) : ProjectImportElement.CreateDisconnected(project, this);
        }
 
        /// <summary>
        /// Creates an item node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectItemElement CreateItemElement(string itemType)
        {
            return Link != null ? RootLink.CreateItemElement(itemType) : ProjectItemElement.CreateDisconnected(itemType, this);
        }
 
        /// <summary>
        /// Creates an item node with an include.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectItemElement CreateItemElement(string itemType, string include)
        {
            if (Link != null)
            {
                return RootLink.CreateItemElement(itemType, include);
            }
 
            ProjectItemElement item = ProjectItemElement.CreateDisconnected(itemType, this);
 
            item.Include = include;
 
            return item;
        }
 
        /// <summary>
        /// Creates an item definition.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectItemDefinitionElement CreateItemDefinitionElement(string itemType)
        {
            return Link != null ? RootLink.CreateItemDefinitionElement(itemType) : ProjectItemDefinitionElement.CreateDisconnected(itemType, this);
        }
 
        /// <summary>
        /// Creates an item definition group.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectItemDefinitionGroupElement CreateItemDefinitionGroupElement()
        {
            return Link != null ? RootLink.CreateItemDefinitionGroupElement() : ProjectItemDefinitionGroupElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates an item group.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectItemGroupElement CreateItemGroupElement()
        {
            return Link != null ? RootLink.CreateItemGroupElement() : ProjectItemGroupElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates an import group.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectImportGroupElement CreateImportGroupElement()
        {
            return Link != null ? RootLink.CreateImportGroupElement() : ProjectImportGroupElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates a metadata node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectMetadataElement CreateMetadataElement(string name)
        {
            return Link != null ? RootLink.CreateMetadataElement(name) : ProjectMetadataElement.CreateDisconnected(name, this);
        }
 
        /// <summary>
        /// Creates a metadata node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue)
        {
            return this.CreateMetadataElement(name, unevaluatedValue, null);
        }
 
        /// <summary>
        /// Creates a metadata node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectMetadataElement CreateMetadataElement(string name, string unevaluatedValue, ElementLocation location)
        {
            if (Link != null)
            {
                return RootLink.CreateMetadataElement(name, unevaluatedValue);
            }
 
            ProjectMetadataElement metadatum = ProjectMetadataElement.CreateDisconnected(name, this, location);
 
            metadatum.Value = unevaluatedValue;
 
            return metadatum;
        }
 
        /// <summary>
        /// Creates an on error node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectOnErrorElement CreateOnErrorElement(string executeTargets)
        {
            return Link != null ? RootLink.CreateOnErrorElement(executeTargets) : ProjectOnErrorElement.CreateDisconnected(executeTargets, this);
        }
 
        /// <summary>
        /// Creates an otherwise node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectOtherwiseElement CreateOtherwiseElement()
        {
            return Link != null ? RootLink.CreateOtherwiseElement() : ProjectOtherwiseElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates an output node.
        /// Exactly one of itemType and propertyName must be specified.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectOutputElement CreateOutputElement(string taskParameter, string itemType, string propertyName)
        {
            return Link != null ? RootLink.CreateOutputElement(taskParameter, itemType, propertyName) : ProjectOutputElement.CreateDisconnected(taskParameter, itemType, propertyName, this);
        }
 
        /// <summary>
        /// Creates a project extensions node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectExtensionsElement CreateProjectExtensionsElement()
        {
            return Link != null ? RootLink.CreateProjectExtensionsElement() : ProjectExtensionsElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates a property group.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectPropertyGroupElement CreatePropertyGroupElement()
        {
            return Link != null ? RootLink.CreatePropertyGroupElement() : ProjectPropertyGroupElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates a property.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        public ProjectPropertyElement CreatePropertyElement(string name)
        {
            return Link != null ? RootLink.CreatePropertyElement(name) : ProjectPropertyElement.CreateDisconnected(name, this);
        }
 
        /// <summary>
        /// Creates a target.
        /// Caller must add it to the location of choice in this project.
        /// </summary>
        public ProjectTargetElement CreateTargetElement(string name)
        {
            return Link != null ? RootLink.CreateTargetElement(name) : ProjectTargetElement.CreateDisconnected(name, this);
        }
 
        /// <summary>
        /// Creates a task.
        /// Caller must add it to the location of choice in this project.
        /// </summary>
        public ProjectTaskElement CreateTaskElement(string name)
        {
            return Link != null ? RootLink.CreateTaskElement(name) : ProjectTaskElement.CreateDisconnected(name, this);
        }
 
        /// <summary>
        /// Creates a using task.
        /// Caller must add it to the location of choice in the project.
        /// Exactly one of assembly file and assembly name must be provided.
        /// </summary>
        public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName)
        {
            return CreateUsingTaskElement(taskName, assemblyFile, assemblyName, null, null);
        }
 
        /// <summary>
        /// Creates a using task.
        /// Caller must add it to the location of choice in the project.
        /// Exactly one of assembly file and assembly name must be provided.
        /// Also allows providing optional runtime and architecture specifiers.  Null is OK.
        /// </summary>
        public ProjectUsingTaskElement CreateUsingTaskElement(string taskName, string assemblyFile, string assemblyName, string runtime, string architecture)
        {
            return Link != null ? RootLink.CreateUsingTaskElement(taskName, assemblyFile, assemblyName, runtime, architecture) : ProjectUsingTaskElement.CreateDisconnected(taskName, assemblyFile, assemblyName, runtime, architecture, this);
        }
 
        /// <summary>
        /// Creates a ParameterGroup for use in a using task.
        /// Caller must add it to the location of choice in the project under a using task.
        /// </summary>
        public UsingTaskParameterGroupElement CreateUsingTaskParameterGroupElement()
        {
            return Link != null ? RootLink.CreateUsingTaskParameterGroupElement() : UsingTaskParameterGroupElement.CreateDisconnected(this);
        }
 
        /// <summary>
        /// Creates a Parameter for use in a using ParameterGroup.
        /// Caller must add it to the location of choice in the project under a using task.
        /// </summary>
        public ProjectUsingTaskParameterElement CreateUsingTaskParameterElement(string name, string output, string required, string parameterType)
        {
            return Link != null ? RootLink.CreateUsingTaskParameterElement(name, output, required, parameterType) : ProjectUsingTaskParameterElement.CreateDisconnected(name, output, required, parameterType, this);
        }
 
        /// <summary>
        /// Creates a Task element for use in a using task.
        /// Caller must add it to the location of choice in the project under a using task.
        /// </summary>
        public ProjectUsingTaskBodyElement CreateUsingTaskBodyElement(string evaluate, string body)
        {
            return Link != null ? RootLink.CreateUsingTaskBodyElement(evaluate, body) : ProjectUsingTaskBodyElement.CreateDisconnected(evaluate, body, this);
        }
 
        /// <summary>
        /// Creates a when.
        /// Caller must add it to the location of choice in this project.
        /// </summary>
        public ProjectWhenElement CreateWhenElement(string condition)
        {
            return Link != null ? RootLink.CreateWhenElement(condition) : ProjectWhenElement.CreateDisconnected(condition, this);
        }
 
        /// <summary>
        /// Creates a project SDK element attached to this project.
        /// </summary>
        public ProjectSdkElement CreateProjectSdkElement(string sdkName, string sdkVersion)
        {
            return Link != null ? RootLink.CreateProjectSdkElement(sdkName, sdkVersion) : ProjectSdkElement.CreateDisconnected(sdkName, sdkVersion, this);
        }
 
        /// <summary>
        /// Save the project to the file system, if dirty.
        /// Uses the Encoding returned by the Encoding property.
        /// Creates any necessary directories.
        /// May throw IO-related exceptions.
        /// Clears the dirty flag.
        /// </summary>
        public void Save()
        {
            Save(Encoding);
        }
 
        /// <summary>
        /// Save the project to the file system, if dirty.
        /// Creates any necessary directories.
        /// May throw IO-related exceptions.
        /// Clears the dirty flag.
        /// </summary>
        public void Save(Encoding saveEncoding)
        {
            if (Link != null)
            {
                RootLink.Save(saveEncoding);
                return;
            }
 
            ErrorUtilities.VerifyThrowInvalidOperation(_projectFileLocation != null, "OM_MustSetFileNameBeforeSave");
 
            Directory.CreateDirectory(DirectoryPath);
 
            // LocationString is normally cheap to calculate, but it can occasionally go down a rabbit hole of method calls. This makes it more consistent if this event is not enabled.
            if (MSBuildEventSource.Log.IsEnabled())
            {
                MSBuildEventSource.Log.SaveStart(_projectFileLocation.LocationString);
            }
            // Note: We're using string Equals on encoding and not EncodingUtilities.SimilarToEncoding in order
            // to force a save if the Encoding changed from UTF8 with BOM to UTF8 w/o BOM (for example).
            if (HasUnsavedChanges || !Equals(saveEncoding, Encoding))
            {
                using (var projectWriter = new ProjectWriter(_projectFileLocation.File, saveEncoding))
                {
                    projectWriter.Initialize(XmlDocument);
                    XmlDocument.Save(projectWriter);
                }
 
                _encoding = saveEncoding;
 
                FileInfo fileInfo = FileUtilities.GetFileInfoNoThrow(_projectFileLocation.File);
 
                // If the file was deleted by a race with someone else immediately after it was written above
                // then we obviously can't read the write time. In this obscure case, we'll retain the
                // older last write time, which at worst would cause the next load to unnecessarily
                // come from disk.
                if (fileInfo != null)
                {
                    _lastWriteTimeWhenReadUtc = fileInfo.LastWriteTimeUtc;
                    if (_lastWriteTimeWhenReadUtc > StreamTimeUtc)
                    {
                        StreamTimeUtc = null;
                    }
                }
 
                _versionOnDisk = Version;
            }
            if (MSBuildEventSource.Log.IsEnabled())
            {
                MSBuildEventSource.Log.SaveStop(_projectFileLocation.LocationString);
            }
        }
 
        /// <summary>
        /// Save the project to the file system, if dirty or the path is different.
        /// Creates any necessary directories.
        /// May throw IO related exceptions.
        /// Clears the Dirty flag.
        /// </summary>
        public void Save(string path)
        {
            Save(path, Encoding);
        }
 
        /// <summary>
        /// Save the project to the file system, if dirty or the path is different.
        /// Creates any necessary directories.
        /// May throw IO related exceptions.
        /// Clears the Dirty flag.
        /// </summary>
        public void Save(string path, Encoding encoding)
        {
            FullPath = path;
 
            Save(encoding);
        }
 
        /// <summary>
        /// Save the project to the provided TextWriter, whether or not it is dirty.
        /// Uses the encoding of the TextWriter.
        /// Clears the Dirty flag.
        /// </summary>
        public void Save(TextWriter writer)
        {
            if (Link != null)
            {
                RootLink.Save(writer);
                return;
            }
 
            using (var projectWriter = new ProjectWriter(writer))
            {
                projectWriter.Initialize(XmlDocument);
                XmlDocument.Save(projectWriter);
            }
 
            StreamTimeUtc = DateTime.UtcNow;
            _versionOnDisk = Version;
        }
 
        /// <summary>
        /// Returns a clone of this project.
        /// </summary>
        /// <returns>The cloned element.</returns>
        public ProjectRootElement DeepClone()
        {
            return (ProjectRootElement)DeepClone(this, null);
        }
 
        /// <summary>
        /// Reload the existing project root element from its file.
        /// An <see cref="InvalidOperationException"/> is thrown if the project root element is not associated with any file on disk.
        ///
        /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/>
        /// </summary>
        public void Reload(bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
        {
            ErrorUtilities.VerifyThrowInvalidOperation(!string.IsNullOrEmpty(FullPath), "ValueNotSet", $"{nameof(ProjectRootElement)}.{nameof(FullPath)}");
 
            ReloadFrom(FullPath, throwIfUnsavedChanges, preserveFormatting);
        }
 
        /// <summary>
        /// Reload the existing project root element from the given path
        /// An <see cref="InvalidOperationException"/> is thrown if the path does not exist.
        ///
        /// See <see cref="ProjectRootElement.ReloadFrom(XmlReader, bool, bool?)"/>
        /// </summary>
        public void ReloadFrom(string path, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
        {
            ErrorUtilities.VerifyThrowInvalidOperation(FileSystems.Default.FileExists(path), "FileToReloadFromDoesNotExist", path);
 
            if (Link != null)
            {
                RootLink.ReloadFrom(path, throwIfUnsavedChanges, preserveFormatting ?? PreserveFormatting);
                return;
            }
 
            XmlDocumentWithLocation DocumentProducer(bool shouldPreserveFormatting) => LoadDocument(path, shouldPreserveFormatting, ProjectRootElementCache.LoadProjectsReadOnly);
            ReloadFrom(DocumentProducer, throwIfUnsavedChanges, preserveFormatting);
        }
 
        /// <summary>
        /// Reload the existing project root element from the given <paramref name="reader"/>
        /// A reload operation completely replaces the state of this <see cref="ProjectRootElement"/> object. This operation marks the
        /// object as dirty.
        ///
        /// If the new state has invalid XML or MSBuild syntax, then this method throws an <see cref="InvalidProjectFileException"/>.
        /// When this happens, the state of this object does not change.
        ///
        /// Reloading from an XMLReader will retain the previous root element location (<see cref="FullPath"/>, <see cref="DirectoryPath"/>, <see cref="ProjectFileLocation"/>).
        ///
        /// </summary>
        /// <param name="reader">Reader to read from</param>
        /// <param name="throwIfUnsavedChanges">
        ///   If set to false, the reload operation will discard any unsaved changes.
        ///   Otherwise, an <see cref="InvalidOperationException"/> is thrown when unsaved changes are present.
        /// </param>
        /// <param name="preserveFormatting">
        ///   Whether the reload should preserve formatting or not. A null value causes the reload to reuse the existing <see cref="PreserveFormatting"/> value.
        /// </param>
        public void ReloadFrom(XmlReader reader, bool throwIfUnsavedChanges = true, bool? preserveFormatting = null)
        {
            if (Link != null)
            {
                RootLink.ReloadFrom(reader, throwIfUnsavedChanges, preserveFormatting ?? PreserveFormatting);
                return;
            }
 
            XmlDocumentWithLocation DocumentProducer(bool shouldPreserveFormatting)
            {
                var document = LoadDocument(reader, shouldPreserveFormatting);
 
                document.FullPath = FullPath;
 
                return document;
            }
 
            ReloadFrom(DocumentProducer, throwIfUnsavedChanges, preserveFormatting);
        }
 
        private void ReloadFrom(Func<bool, XmlDocumentWithLocation> documentProducer, bool throwIfUnsavedChanges, bool? preserveFormatting)
        {
            ThrowIfUnsavedChanges(throwIfUnsavedChanges);
 
            var oldDocument = XmlDocument;
            XmlDocumentWithLocation newDocument = documentProducer(preserveFormatting ?? PreserveFormatting);
 
            // Reload should only mutate the state if there are no parse errors.
            ThrowIfDocumentHasParsingErrors(newDocument);
 
            RemoveAllChildren();
 
            ProjectParser.Parse(newDocument, this);
 
            MarkDirty("Project reloaded", null);
        }
 
        [MethodImpl(MethodImplOptions.NoOptimization)]
        private static void ThrowIfDocumentHasParsingErrors(XmlDocumentWithLocation document)
        {
            // todo: rather than throw away, copy over the parse results
            var throwaway = new ProjectRootElement(document);
        }
 
        /// <summary>
        /// Initialize an in-memory, empty ProjectRootElement instance that can be saved later.
        /// Uses the specified project root element cache.
        /// </summary>
        internal static ProjectRootElement Create(ProjectRootElementCacheBase projectRootElementCache)
        {
            return new ProjectRootElement(projectRootElementCache, Project.DefaultNewProjectTemplateOptions);
        }
 
        internal static ProjectRootElement Create(ProjectRootElementCacheBase projectRootElementCache, NewProjectFileOptions projectFileOptions)
        {
            return new ProjectRootElement(projectRootElementCache, projectFileOptions);
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance by loading from the specified file path.
        /// Assumes path is already normalized.
        /// Uses the specified project root element cache.
        /// May throw InvalidProjectFileException.
        /// </summary>
        internal static ProjectRootElement Open(string path, ProjectRootElementCacheBase projectRootElementCache, bool isExplicitlyLoaded,
            bool? preserveFormatting)
        {
            ErrorUtilities.VerifyThrowInternalRooted(path);
 
            ProjectRootElement projectRootElement = projectRootElementCache.Get(path,
                preserveFormatting ?? false ? s_openLoaderPreserveFormattingDelegate : s_openLoaderDelegate,
                isExplicitlyLoaded, preserveFormatting);
 
            return projectRootElement;
        }
 
        /// <summary>
        /// Initialize a ProjectRootElement instance from an existing document.
        /// Uses the global project collection.
        /// May throw InvalidProjectFileException.
        /// </summary>
        /// <remarks>
        /// This is ultimately for unit testing.
        /// Do not make public: we do not wish to expose particular XML API's.
        /// </remarks>
        internal static ProjectRootElement Open(XmlDocumentWithLocation document)
        {
            ErrorUtilities.VerifyThrow(document.FullPath == null, "Only virtual documents supported");
 
            return new ProjectRootElement(document, ProjectCollection.GlobalProjectCollection.ProjectRootElementCache);
        }
 
        /// <summary>
        /// Gets a ProjectRootElement representing an MSBuild file.
        /// Path provided must be a canonicalized full path.
        /// May throw InvalidProjectFileException or an IO-related exception.
        /// </summary>
        internal static ProjectRootElement OpenProjectOrSolution(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion, ProjectRootElementCacheBase projectRootElementCache, bool isExplicitlyLoaded)
        {
            ErrorUtilities.VerifyThrowInternalRooted(fullPath);
 
            ProjectRootElement projectRootElement = projectRootElementCache.Get(
                fullPath,
                (path, cache) => CreateProjectFromPath(path, cache, preserveFormatting: false),
                isExplicitlyLoaded,
                // don't care about formatting, reuse whatever is there
                preserveFormatting: null);
 
            return projectRootElement;
        }
 
        /// <summary>
        /// Creates a metadata node.
        /// Caller must add it to the location of choice in the project.
        /// </summary>
        internal ProjectMetadataElement CreateMetadataElement(XmlAttributeWithLocation attribute)
        {
            return CreateMetadataElement(attribute.Name, attribute.Value, attribute.Location);
        }
 
        /// <summary>
        /// Creates a XmlElement with the specified name in the document
        /// containing this project.
        /// </summary>
        internal XmlElementWithLocation CreateElement(string name, ElementLocation location = null)
        {
            ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed.");
            return (XmlElementWithLocation)XmlDocument.CreateElement(name, XmlNamespace, location);
        }
 
        /// <summary>
        /// Overridden to verify that the potential parent and siblings
        /// are acceptable. Throws InvalidOperationException if they are not.
        /// </summary>
        internal override void VerifyThrowInvalidOperationAcceptableLocation(ProjectElementContainer parent, ProjectElement previousSibling, ProjectElement nextSibling)
        {
            ErrorUtilities.ThrowInvalidOperation("OM_CannotAcceptParent");
        }
 
        /// <summary>
        /// Marks this project as dirty.
        /// Typically called by child elements to indicate that they themselves have become dirty.
        /// Accepts a reason for debugging purposes only, and optional reason parameter.
        /// </summary>
        /// <remarks>
        /// This is sealed because it is virtual and called in a constructor; by sealing it we
        /// satisfy the rule that nobody will override it to do something that would rely on
        /// unconstructed state.
        /// Should be protected+internal.
        /// </remarks>
        internal sealed override void MarkDirty(string reason, string param)
        {
            if (Link != null)
            {
                RootLink.MarkDirty(reason, param);
                return;
            }
 
            IncrementVersion();
 
            _dirtyReason = reason;
            _dirtyParameter = param;
 
            _timeLastChangedUtc = DateTime.UtcNow;
 
            var changedEventArgs = new ProjectXmlChangedEventArgs(this, reason, param);
            EventHandler<ProjectXmlChangedEventArgs> projectXmlChanged = OnProjectXmlChanged;
            projectXmlChanged?.Invoke(this, changedEventArgs);
 
            // Only bubble this event up if the cache knows about this PRE.
            if (IsMemberOfProjectCollection)
            {
                ProjectRootElementCache.OnProjectRootElementDirtied(this, changedEventArgs);
            }
        }
 
        /// <summary>
        /// Bubbles a Project dirty notification up to the ProjectRootElementCacheBase and ultimately to the ProjectCollection.
        /// </summary>
        /// <param name="project">The dirtied project.</param>
        internal void MarkProjectDirty(Project project)
        {
            ErrorUtilities.VerifyThrowArgumentNull(project);
            ErrorUtilities.VerifyThrow(Link == null, "Attempt to edit a document that is not backed by a local xml is disallowed.");
 
            // Only bubble this event up if the cache knows about this PRE, which is equivalent to
            // whether this PRE has a path.
            if (_projectFileLocation != null)
            {
                ProjectRootElementCache.OnProjectDirtied(project, new ProjectChangedEventArgs(project));
            }
        }
 
        /// <summary>
        /// Sets the <see cref="IsExplicitlyLoaded"/> property to <c>true</c> to indicate that this PRE
        /// should not be removed from the cache until it is explicitly unloaded by some MSBuild client.
        /// </summary>
        internal void MarkAsExplicitlyLoaded()
        {
            IsExplicitlyLoaded = true;
        }
 
        /// <summary>
        /// Creates and returns a list of <see cref="ProjectImportElement"/> nodes which are implicitly
        /// referenced by the Project.
        /// </summary>
        /// <param name="currentProjectOrImport">Current project</param>
        /// <returns>An <see cref="IEnumerable{SdkReference}"/> containing details of the SDKs referenced by the project.</returns>
        internal List<ProjectImportElement> GetImplicitImportNodes(ProjectRootElement currentProjectOrImport)
        {
            var nodes = new List<ProjectImportElement>();
 
            string sdkAttribute = Sdk;
            if (!string.IsNullOrWhiteSpace(sdkAttribute))
            {
                foreach (var referencedSdk in ParseSdks(sdkAttribute, SdkLocation))
                {
                    nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk, this));
                    nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk, this));
                }
            }
 
            foreach (ProjectElement child in ChildrenEnumerable)
            {
                if (child is ProjectSdkElement sdkNode)
                {
                    var referencedSdk = new SdkReference(
                        sdkNode.XmlElement.GetAttribute("Name"),
                        sdkNode.XmlElement.GetAttribute("Version"),
                        sdkNode.XmlElement.GetAttribute("MinimumVersion"));
 
                    nodes.Add(ProjectImportElement.CreateImplicit("Sdk.props", currentProjectOrImport, ImplicitImportLocation.Top, referencedSdk, sdkNode));
                    nodes.Add(ProjectImportElement.CreateImplicit("Sdk.targets", currentProjectOrImport, ImplicitImportLocation.Bottom, referencedSdk, sdkNode));
                }
            }
 
            return nodes;
        }
 
        private static IEnumerable<SdkReference> ParseSdks(string sdks, IElementLocation sdkLocation)
        {
            foreach (string sdk in sdks.Split(MSBuildConstants.SemicolonChar).Select(i => i.Trim()))
            {
                if (!SdkReference.TryParse(sdk, out SdkReference sdkReference))
                {
                    ProjectErrorUtilities.ThrowInvalidProject(sdkLocation, "InvalidSdkFormat", sdks);
                }
 
                yield return sdkReference;
            }
        }
 
        /// <summary>
        /// Determines if the specified file is an empty XML file meaning it has no contents, contains only whitespace, or
        /// only an XML declaration.  If the file does not exist, it is not considered empty.
        /// </summary>
        /// <param name="path">The full path to a file to check.</param>
        /// <returns><code>true</code> if the file is an empty XML file, otherwise <code>false</code>.</returns>
        internal static bool IsEmptyXmlFile(string path)
        {
            // The maximum number of characters of the file to read to check if its empty or not.  Ideally we
            // would only look at zero-length files but empty XML files can contain just an xml declaration:
            //
            //   <? xml version="1.0" encoding="utf-8" standalone="yes" ?>
            //
            // And this should also be treated as if the file is empty.
            //
            const int maxSizeToConsiderEmpty = 100;
 
            if (!FileSystems.Default.FileExists(path))
            {
                // Non-existent files are not treated as empty
                //
                return false;
            }
 
            try
            {
                FileInfo fileInfo = new FileInfo(path);
 
                if (fileInfo.Length == 0)
                {
                    // Zero length files are empty
                    //
                    return true;
                }
 
                if (fileInfo.Length > maxSizeToConsiderEmpty)
                {
                    // Files greater than the maximum bytes to check are not empty
                    //
                    return false;
                }
 
                string contents = File.ReadAllText(path);
 
                // If the file is only whitespace or the XML declaration then it empty
                //
                return String.IsNullOrEmpty(contents) || XmlDeclarationRegEx.Value.IsMatch(contents);
            }
            catch (Exception)
            {
                // ignored
            }
 
            return false;
        }
 
        /// <summary>
        /// Returns a new instance of ProjectRootElement that is affiliated with the same ProjectRootElementCache.
        /// </summary>
        /// <param name="owner">The factory to use for creating the new instance.</param>
        protected override ProjectElement CreateNewInstance(ProjectRootElement owner)
        {
            return Link != null ? Link.CreateNewInstance(owner) : Create(owner.ProjectRootElementCache);
        }
 
        /// <summary>
        /// Creates a new ProjectRootElement for a specific PRE cache
        /// </summary>
        /// <param name="path">The path to the file to load.</param>
        /// <param name="projectRootElementCache">The cache to load the PRE into.</param>
        private static ProjectRootElement OpenLoader(string path, ProjectRootElementCacheBase projectRootElementCache)
        {
            return OpenLoader(path, projectRootElementCache, preserveFormatting: false);
        }
 
        private static ProjectRootElement OpenLoaderPreserveFormatting(string path, ProjectRootElementCacheBase projectRootElementCache)
        {
            return OpenLoader(path, projectRootElementCache, preserveFormatting: true);
        }
 
        private static ProjectRootElement OpenLoader(string path, ProjectRootElementCacheBase projectRootElementCache, bool preserveFormatting)
        {
            return new ProjectRootElement(
                path,
                projectRootElementCache,
                preserveFormatting);
        }
 
        /// <summary>
        /// Creates a ProjectRootElement representing a file, where the file may be a .sln instead of
        /// an MSBuild format file.
        /// Assumes path is already normalized.
        /// If the file is in MSBuild format, may throw InvalidProjectFileException.
        /// If the file is a solution, will throw an IO-related exception if the file cannot be read.
        /// </summary>
        private static ProjectRootElement CreateProjectFromPath(
                string projectFile,
                ProjectRootElementCacheBase projectRootElementCache,
                bool preserveFormatting)
        {
            ErrorUtilities.VerifyThrowInternalRooted(projectFile);
 
            try
            {
                if (FileUtilities.IsVCProjFilename(projectFile))
                {
                    ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), "ProjectUpgradeNeededToVcxProj", projectFile);
                }
 
                // OK it's a regular project file, load it normally.
                return new ProjectRootElement(projectFile, projectRootElementCache, preserveFormatting);
            }
            catch (InvalidProjectFileException)
            {
                throw;
            }
            catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
            {
                ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(projectFile), ex, "InvalidProjectFile", ex.Message);
                throw; // Without this there's a spurious CS0161 because csc 1.2.0.60317 can't see that the above is an unconditional throw.
            }
        }
 
        /// <summary>
        /// Constructor helper to load an XmlDocumentWithLocation from a path.
        /// Assumes path is already normalized.
        /// May throw InvalidProjectFileException.
        /// Never returns null.
        /// Does NOT add to the ProjectRootElementCache. Caller should add after verifying subsequent MSBuild parsing succeeds.
        /// </summary>
        /// <param name="fullPath">The full path to the document to load.</param>
        /// <param name="preserveFormatting"><code>true</code> to preserve the formatting of the document, otherwise <code>false</code>.</param>
        /// <param name="loadAsReadOnly">Whether to load the file in read-only mode.</param>
        private XmlDocumentWithLocation LoadDocument(string fullPath, bool preserveFormatting, bool loadAsReadOnly)
        {
            ErrorUtilities.VerifyThrowInternalRooted(fullPath);
 
            var document = new XmlDocumentWithLocation(loadAsReadOnly ? true : (bool?)null)
            {
                FullPath = fullPath,
                PreserveWhitespace = preserveFormatting
            };
 
            try
            {
                MSBuildEventSource.Log.LoadDocumentStart(fullPath);
                using (XmlReaderExtension xtr = XmlReaderExtension.Create(fullPath, loadAsReadOnly))
                {
                    _encoding = xtr.Encoding;
                    document.Load(xtr.Reader);
                }
 
                _projectFileLocation = ElementLocation.Create(fullPath);
                _escapedFullPath = null;
                _directory = Path.GetDirectoryName(fullPath);
 
                if (XmlDocument != null)
                {
                    XmlDocument.FullPath = fullPath;
                }
 
                _lastWriteTimeWhenReadUtc = FileUtilities.GetFileInfoNoThrow(fullPath).LastWriteTimeUtc;
                if (StreamTimeUtc < _lastWriteTimeWhenReadUtc)
                {
                    StreamTimeUtc = null;
                }
            }
            catch (Exception ex) when (!ExceptionHandling.NotExpectedIoOrXmlException(ex))
            {
                BuildEventFileInfo fileInfo = ex is XmlException xmlException
                    ? new BuildEventFileInfo(fullPath, xmlException)
                    : new BuildEventFileInfo(fullPath);
 
                ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, ex, "InvalidProjectFile", ex.Message);
            }
            MSBuildEventSource.Log.LoadDocumentStop(fullPath);
 
            return document;
        }
 
        /// <summary>
        /// Constructor helper to load an XmlDocumentWithLocation from an XmlReader.
        /// May throw InvalidProjectFileException.
        /// Never returns null.
        /// </summary>
        private static XmlDocumentWithLocation LoadDocument(XmlReader reader, bool preserveFormatting)
        {
            var document = new XmlDocumentWithLocation { PreserveWhitespace = preserveFormatting };
 
            try
            {
                document.Load(reader);
            }
            catch (XmlException ex)
            {
                BuildEventFileInfo fileInfo = new BuildEventFileInfo(ex);
 
                ProjectFileErrorUtilities.ThrowInvalidProjectFile(fileInfo, "InvalidProjectFile", ex.Message);
            }
 
            return document;
        }
 
        /// <summary>
        /// Boost the appdomain-unique version counter for this object.
        /// This is done when it is modified, and also when it is loaded.
        /// </summary>
        private void IncrementVersion()
        {
            Version = Interlocked.Increment(ref s_globalVersionCounter);
        }
 
        private void ThrowIfUnsavedChanges(bool throwIfUnsavedChanges)
        {
            if (HasUnsavedChanges && throwIfUnsavedChanges)
            {
                ErrorUtilities.ThrowInvalidOperation("NoReloadOnUnsavedChanges", null);
            }
        }
    }
}