File: Definition\ProjectCollection.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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;
using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord;
using ILoggingService = Microsoft.Build.BackEnd.Logging.ILoggingService;
using InternalLoggerException = Microsoft.Build.Exceptions.InternalLoggerException;
using InvalidProjectFileException = Microsoft.Build.Exceptions.InvalidProjectFileException;
using LoggerMode = Microsoft.Build.BackEnd.Logging.LoggerMode;
using ObjectModel = System.Collections.ObjectModel;
 
#nullable disable
 
namespace Microsoft.Build.Evaluation
{
    using Utilities = Microsoft.Build.Internal.Utilities;
 
    /// <summary>
    /// Flags for controlling the toolset initialization.
    /// </summary>
    [Flags]
    public enum ToolsetDefinitionLocations
    {
        /// <summary>
        /// Do not read toolset information from any external location.
        /// </summary>
        None = 0,
 
        /// <summary>
        /// Read toolset information from the exe configuration file.
        /// </summary>
        ConfigurationFile = 1,
 
        /// <summary>
        /// Read toolset information from the registry (HKLM\Software\Microsoft\MSBuild\ToolsVersions).
        /// </summary>
        Registry = 2,
 
        /// <summary>
        /// Read toolset information from the current exe path
        /// </summary>
        Local = 4,
 
        /// <summary>
        /// Use the default location or locations.
        /// </summary>
        Default = None
#if FEATURE_SYSTEM_CONFIGURATION
                | ConfigurationFile
#endif
#if FEATURE_REGISTRY_TOOLSETS
                | Registry
#endif
#if !FEATURE_SYSTEM_CONFIGURATION && !FEATURE_REGISTRY_TOOLSETS
                | Local
#endif
    }
 
    /// <summary>
    /// This class encapsulates a set of related projects, their toolsets, a default set of global properties,
    /// and the loggers that should be used to build them.
    /// A global version of this class acts as the default ProjectCollection.
    /// Multiple ProjectCollections can exist within an appdomain. However, these must not build concurrently.
    /// </summary>
    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix", Justification = "This is a collection of projects API review has approved this")]
    public class ProjectCollection : IToolsetProvider, IBuildComponent, IDisposable
    {
        /// <summary>
        /// The object to synchronize with when accessing certain fields.
        /// </summary>
        /// <remarks>
        /// ProjectCollection is highly reentrant - project creation, toolset and logger changes, and so on
        /// all need lock protection, but there are a lot of read cases as well, and calls to create Projects
        /// call back to the ProjectCollection under locks. Use a RW lock with recursion support to avoid
        /// adding reentrancy bugs.
        /// </remarks>
        private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
 
        /// <summary>
        /// The global singleton project collection used as a default for otherwise
        /// unassociated projects.
        /// </summary>
        private static ProjectCollection s_globalProjectCollection;
 
        /// <summary>
        /// Gets the file version of the file in which the Engine assembly lies.
        /// </summary>
        /// <remarks>
        /// This is the Windows file version (specifically the value of the ProductVersion
        /// resource), not necessarily the assembly version.
        /// If you want the assembly version, use Constants.AssemblyVersion.
        /// </remarks>
        private static Version s_engineVersion;
 
        /// <summary>
        /// The display version of the file in which the Engine assembly lies.
        /// </summary>
        private static string s_assemblyDisplayVersion;
 
        private static ProjectRootElementCacheBase s_projectRootElementCache = null;
 
        /// <summary>
        /// The projects loaded into this collection.
        /// </summary>
        private readonly LoadedProjectCollection _loadedProjects;
 
        /// <summary>
        /// External projects support
        /// </summary>
        private ExternalProjectsProvider _link;
 
        /// <summary>
        /// Single logging service used for all builds of projects in this project collection
        /// </summary>
        private ILoggingService _loggingService;
 
        /// <summary>
        /// Any object exposing host services.
        /// May be null.
        /// </summary>
        private HostServices _hostServices;
 
        /// <summary>
        /// A mapping of tools versions to Toolsets, which contain the public Toolsets.
        /// This is the collection we use internally.
        /// </summary>
        private Dictionary<string, Toolset> _toolsets;
 
        /// <summary>
        /// The default global properties.
        /// </summary>
        private readonly PropertyDictionary<ProjectPropertyInstance> _globalProperties;
 
        /// <summary>
        /// The properties representing the environment.
        /// </summary>
        private PropertyDictionary<ProjectPropertyInstance> _environmentProperties;
 
        /// <summary>
        /// The default tools version obtained by examining all of the toolsets.
        /// </summary>
        private string _defaultToolsVersion;
 
        /// <summary>
        /// A counter incremented every time the toolsets change which would necessitate a re-evaluation of
        /// associated projects.
        /// </summary>
        private int _toolsetsVersion;
 
        /// <summary>
        /// This is the default value used by newly created projects for whether or not the building
        /// of projects is enabled.  This is for security purposes in case a host wants to closely
        /// control which projects it allows to run targets/tasks.
        /// </summary>
        private bool _isBuildEnabled = true;
 
        /// <summary>
        /// We may only wish to log critical events, record that fact so we can apply it to build requests
        /// </summary>
        private bool _onlyLogCriticalEvents;
 
        /// <summary>
        /// Whether reevaluation is temporarily disabled on projects in this collection.
        /// This is useful when the host expects to make a number of reads and writes
        /// to projects, and wants to temporarily sacrifice correctness for performance.
        /// </summary>
        private bool _skipEvaluation;
 
        /// <summary>
        /// Whether <see cref="Project.MarkDirty()">MarkDirty()</see> is temporarily disabled on
        /// projects in this collection.
        /// This allows, for example, global properties to be set without projects getting
        /// marked dirty for reevaluation as a consequence.
        /// </summary>
        private bool _disableMarkDirty;
 
        /// <summary>
        /// The maximum number of nodes which can be started during the build
        /// </summary>
        private int _maxNodeCount;
 
        /// <summary>
        /// LoggingService Logger mode.
        /// If Asynchronous mode is used
        /// </summary>
        private LoggerMode _loggerMode;
 
        /// <summary>
        /// Instantiates a project collection with no global properties or loggers that reads toolset
        /// information from the configuration file and registry.
        /// </summary>
        public ProjectCollection()
            : this(null)
        {
        }
 
        /// <summary>
        /// Instantiates a project collection using toolsets from the specified locations,
        /// and no global properties or loggers.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="toolsetLocations">The locations from which to load toolsets.</param>
        public ProjectCollection(ToolsetDefinitionLocations toolsetLocations)
            : this(null, null, toolsetLocations)
        {
        }
 
        /// <summary>
        /// Instantiates a project collection with specified global properties, no loggers,
        /// and that reads toolset information from the configuration file and registry.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties)
            : this(globalProperties, null, ToolsetDefinitionLocations.Default)
        {
        }
 
        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, ToolsetDefinitionLocations toolsetDefinitionLocations)
            : this(globalProperties, loggers, null, toolsetDefinitionLocations, 1 /* node count */, false /* do not only log critical events */)
        {
        }
 
        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents)
            : this(globalProperties, loggers, null, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly: false)
        {
        }
 
        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        /// <param name="loadProjectsReadOnly">If set to true, load all projects as read-only.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly)
            : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, useAsynchronousLogging: false, reuseProjectRootElementCache: false)
        {
        }
 
 
        /// <summary>
        /// Instantiates a project collection with specified global properties and loggers and using the
        /// specified toolset locations, node count, and setting of onlyLogCriticalEvents.
        /// Global properties and loggers may be null.
        /// Throws InvalidProjectFileException if any of the global properties are reserved.
        /// May throw InvalidToolsetDefinitionException.
        /// </summary>
        /// <param name="globalProperties">The default global properties to use. May be null.</param>
        /// <param name="loggers">The loggers to register. May be null and specified to any build instead.</param>
        /// <param name="remoteLoggers">Any remote loggers to register. May be null and specified to any build instead.</param>
        /// <param name="toolsetDefinitionLocations">The locations from which to load toolsets.</param>
        /// <param name="maxNodeCount">The maximum number of nodes to use for building.</param>
        /// <param name="onlyLogCriticalEvents">If set to true, only critical events will be logged.</param>
        /// <param name="loadProjectsReadOnly">If set to true, load all projects as read-only.</param>
        /// <param name="useAsynchronousLogging">If set to true, asynchronous logging will be used. <see cref="ProjectCollection.Dispose()"/> has to called to clear resources used by async logging.</param>
        /// <param name="reuseProjectRootElementCache">If set to true, it will try to reuse <see cref="ProjectRootElementCacheBase"/> singleton.</param>
        public ProjectCollection(IDictionary<string, string> globalProperties, IEnumerable<ILogger> loggers, IEnumerable<ForwardingLoggerRecord> remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging, bool reuseProjectRootElementCache)
        {
            _loadedProjects = new LoadedProjectCollection();
            ToolsetLocations = toolsetDefinitionLocations;
            MaxNodeCount = maxNodeCount;
 
            if (Traits.Instance.UseSimpleProjectRootElementCacheConcurrency)
            {
                ProjectRootElementCache = new SimpleProjectRootElementCache();
            }
            else if (reuseProjectRootElementCache && s_projectRootElementCache != null)
            {
                ProjectRootElementCache = s_projectRootElementCache;
            }
            else
            {
                // When we are reusing ProjectRootElementCache we need to reload XMLs if it has changed between MSBuild Server sessions/builds.
                // If we are not reusing, cache will be released at end of build and as we do not support project files will changes during build
                // we do not need to auto reload.
                bool autoReloadFromDisk = reuseProjectRootElementCache;
                ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly);
                if (reuseProjectRootElementCache)
                {
                    s_projectRootElementCache = ProjectRootElementCache;
                }
            }
 
            OnlyLogCriticalEvents = onlyLogCriticalEvents;
 
            try
            {
                _loggerMode = useAsynchronousLogging ? LoggerMode.Asynchronous : LoggerMode.Synchronous;
                CreateLoggingService(maxNodeCount, onlyLogCriticalEvents);
 
                RegisterLoggers(loggers);
                RegisterForwardingLoggers(remoteLoggers);
 
                if (globalProperties != null)
                {
                    _globalProperties = new PropertyDictionary<ProjectPropertyInstance>(globalProperties.Count);
 
                    foreach (KeyValuePair<string, string> pair in globalProperties)
                    {
                        try
                        {
                            _globalProperties.Set(ProjectPropertyInstance.Create(pair.Key, pair.Value));
                        }
                        catch (ArgumentException ex)
                        {
                            // Reserved or invalid property name
                            try
                            {
                                ProjectErrorUtilities.ThrowInvalidProject(ElementLocation.Create("MSBUILD"), "InvalidProperty", ex.Message);
                            }
                            catch (InvalidProjectFileException ex2)
                            {
                                BuildEventContext buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId);
                                LoggingService.LogInvalidProjectFileError(buildEventContext, ex2);
                                throw;
                            }
                        }
                    }
                }
                else
                {
                    _globalProperties = new PropertyDictionary<ProjectPropertyInstance>();
                }
 
                InitializeToolsetCollection();
            }
            catch (Exception)
            {
                ShutDownLoggingService();
                throw;
            }
 
            ProjectRootElementCache.ProjectRootElementAddedHandler += ProjectRootElementCache_ProjectRootElementAddedHandler;
            ProjectRootElementCache.ProjectRootElementDirtied += ProjectRootElementCache_ProjectRootElementDirtiedHandler;
            ProjectRootElementCache.ProjectDirtied += ProjectRootElementCache_ProjectDirtiedHandler;
        }
 
        /// <summary>
        /// Handler to receive which project got added to the project collection.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")]
        public delegate void ProjectAddedEventHandler(object sender, ProjectAddedToProjectCollectionEventArgs e);
 
        /// <summary>
        /// Event that is fired when a project is added to the ProjectRootElementCache of this project collection.
        /// </summary>
        public event ProjectAddedEventHandler ProjectAdded;
 
        /// <summary>
        /// Raised when state is changed on this instance.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for changes in individual projects.
        /// </remarks>
        public event EventHandler<ProjectCollectionChangedEventArgs> ProjectCollectionChanged;
 
        /// <summary>
        /// Raised when a <see cref="ProjectRootElement"/> contained by this instance is changed.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for changes to global properties, or any other change that doesn't actually dirty the XML.
        /// </remarks>
        public event EventHandler<ProjectXmlChangedEventArgs> ProjectXmlChanged;
 
        /// <summary>
        /// Raised when a <see cref="Project"/> contained by this instance is directly changed.
        /// </summary>
        /// <remarks>
        /// This event is NOT raised for direct project XML changes via the construction model.
        /// </remarks>
        public event EventHandler<ProjectChangedEventArgs> ProjectChanged;
 
        /// <summary>
        /// Retrieves the global project collection object.
        /// This is a singleton project collection with no global properties or loggers that reads toolset
        /// information from the configuration file and registry.
        /// May throw InvalidToolsetDefinitionException.
        /// Thread safe.
        /// </summary>
        public static ProjectCollection GlobalProjectCollection
        {
            get
            {
                if (s_globalProjectCollection == null)
                {
                    // Take care to ensure that there is never more than one value observed
                    // from this property even in the case of race conditions while lazily initializing.
                    var local = new ProjectCollection(null, null, null, ToolsetDefinitionLocations.Default,
                        maxNodeCount: 1, onlyLogCriticalEvents: false, loadProjectsReadOnly: false, useAsynchronousLogging: true, reuseProjectRootElementCache: false);
 
                    if (Interlocked.CompareExchange(ref s_globalProjectCollection, local, null) != null)
                    {
                        // Other thread beat us to it; dispose of this project collection
                        local.Dispose();
                    }
                }
 
                return s_globalProjectCollection;
            }
        }
 
        /// <summary>
        /// Gets the file version of the file in which the Engine assembly lies.
        /// </summary>
        /// <remarks>
        /// This is the Windows file version (specifically the value of the FileVersion
        /// resource), not necessarily the assembly version.
        /// If you want the assembly version, use Constants.AssemblyVersion.
        /// This is not the <see cref="Toolset.ToolsVersion"/>.
        /// </remarks>
        public static Version Version
        {
            get
            {
                if (s_engineVersion == null)
                {
                    // Get the file version from the currently executing assembly.
                    // Use .CodeBase instead of .Location, because .Location doesn't
                    // work when Microsoft.Build.dll has been shadow-copied, for example
                    // in scenarios where NUnit is loading Microsoft.Build.
                    var versionInfo = FileVersionInfo.GetVersionInfo(FileUtilities.ExecutingAssemblyPath);
                    s_engineVersion = new Version(versionInfo.FileMajorPart, versionInfo.FileMinorPart, versionInfo.FileBuildPart, versionInfo.FilePrivatePart);
                }
 
                return s_engineVersion;
            }
        }
 
        /// <summary>
        /// Gets a version of the Engine suitable for display to a user.
        /// </summary>
        /// <remarks>
        /// This is in the form of a SemVer v2 version, Major.Minor.Patch-prerelease+metadata.
        /// </remarks>
        public static string DisplayVersion
        {
            get
            {
                if (s_assemblyDisplayVersion == null)
                {
                    var fullInformationalVersion = typeof(Constants).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
 
                    // use a truncated version with only 9 digits of SHA
                    var plusIndex = fullInformationalVersion.IndexOf('+');
                    s_assemblyDisplayVersion = plusIndex < 0
                                                    ? fullInformationalVersion
                                                    : fullInformationalVersion.Substring(startIndex: 0, length: plusIndex + 10);
                }
 
                return s_assemblyDisplayVersion;
            }
        }
 
        /// <summary>
        /// The default tools version of this project collection. Projects use this tools version if they
        /// aren't otherwise told what tools version to use.
        /// This value is gotten from the .exe.config file, or else in the registry,
        /// or if neither specify a default tools version then it is hard-coded to the tools version "2.0".
        /// Setter throws InvalidOperationException if a toolset with the provided tools version has not been defined.
        /// Always defined.
        /// </summary>
        public string DefaultToolsVersion
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    ErrorUtilities.VerifyThrow(_defaultToolsVersion != null, "Should have a default");
                    return _defaultToolsVersion;
                }
            }
 
            set
            {
                ErrorUtilities.VerifyThrowArgumentLength(value, nameof(DefaultToolsVersion));
 
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (!_toolsets.ContainsKey(value))
                    {
                        string toolsVersionList = Utilities.CreateToolsVersionListString(Toolsets);
                        ErrorUtilities.ThrowInvalidOperation("UnrecognizedToolsVersion", value, toolsVersionList);
                    }
 
                    if (_defaultToolsVersion != value)
                    {
                        _defaultToolsVersion = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DefaultToolsVersion));
                }
            }
        }
 
        /// <summary>
        /// Returns default global properties for all projects in this collection.
        /// Read-only dead dictionary.
        /// </summary>
        /// <remarks>
        /// This is the publicly exposed getter, that translates into a read-only dead IDictionary&lt;string, string&gt;.
        ///
        /// To be consistent with Project, setting and removing global properties is done with
        /// <see cref="SetGlobalProperty">SetGlobalProperty</see> and <see cref="RemoveGlobalProperty">RemoveGlobalProperty</see>.
        /// </remarks>
        public IDictionary<string, string> GlobalProperties
        {
            get
            {
                Dictionary<string, string> dictionary;
 
                using (_locker.EnterDisposableReadLock())
                {
                    if (_globalProperties.Count == 0)
                    {
                        return ReadOnlyEmptyDictionary<string, string>.Instance;
                    }
 
                    dictionary = new Dictionary<string, string>(_globalProperties.Count, MSBuildNameIgnoreCaseComparer.Default);
 
                    foreach (ProjectPropertyInstance property in _globalProperties)
                    {
                        dictionary[property.Name] = ((IProperty)property).EvaluatedValueEscaped;
                    }
                }
 
                return new ObjectModel.ReadOnlyDictionary<string, string>(dictionary);
            }
        }
 
        /// <summary>
        /// All the projects currently loaded into this collection.
        /// Each has a unique combination of path, global properties, and tools version.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
        public ICollection<Project> LoadedProjects => GetLoadedProjects(true, null);
 
        /// <summary>
        /// Number of projects currently loaded into this collection.
        /// </summary>
        public int Count
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loadedProjects.Count;
                }
            }
        }
 
        /// <summary>
        /// Loggers that all contained projects will use for their builds.
        /// Loggers are added with the <see cref="RegisterLogger"/>.
        /// Returns an empty collection if there are no loggers.
        /// </summary>
        // UNDONE: Currently loggers cannot be removed.
        public ICollection<ILogger> Loggers
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loggingService.Loggers == null
                        ? (ICollection<ILogger>)ReadOnlyEmptyCollection<ILogger>.Instance
                        : new List<ILogger>(_loggingService.Loggers);
                }
            }
        }
 
        /// <summary>
        /// Returns the toolsets this ProjectCollection knows about.
        /// </summary>
        /// <comments>
        /// ValueCollection is already read-only
        /// </comments>
        public ICollection<Toolset> Toolsets
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return new List<Toolset>(_toolsets.Values);
                }
            }
        }
 
        /// <summary>
        /// Returns the locations used to find the toolsets.
        /// </summary>
        public ToolsetDefinitionLocations ToolsetLocations { get; }
 
        /// <summary>
        /// This is the default value used by newly created projects for whether or not the building
        /// of projects is enabled.  This is for security purposes in case a host wants to closely
        /// control which projects it allows to run targets/tasks.
        /// </summary>
        public bool IsBuildEnabled
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _isBuildEnabled;
                }
            }
 
            [DebuggerStepThrough]
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_isBuildEnabled != value)
                    {
                        _isBuildEnabled = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.IsBuildEnabled));
                }
            }
        }
 
        /// <summary>
        /// When true, only log critical events such as warnings and errors. Has to be in here for API compat
        /// </summary>
        public bool OnlyLogCriticalEvents
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _onlyLogCriticalEvents;
                }
            }
 
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_onlyLogCriticalEvents != value)
                    {
                        _onlyLogCriticalEvents = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.OnlyLogCriticalEvents));
                }
            }
        }
 
        /// <summary>
        /// Object exposing host services to tasks during builds of projects
        /// contained by this project collection.
        /// By default, <see cref="HostServices">HostServices</see> is used.
        /// May be set to null, but the getter will create a new instance in that case.
        /// </summary>
        public HostServices HostServices
        {
            get
            {
                // Avoid write lock if possible, this getter is called a lot during Project construction.
                using (_locker.EnterDisposableReadLock())
                {
                    if (_hostServices != null)
                    {
                        return _hostServices;
                    }
                }
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_hostServices == null)
                    {
                        _hostServices = new HostServices();
                    }
                    return _hostServices;
                }
            }
 
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_hostServices != value)
                    {
                        _hostServices = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.HostServices));
                }
            }
        }
 
        /// <summary>
        /// Whether reevaluation is temporarily disabled on projects in this collection.
        /// This is useful when the host expects to make a number of reads and writes
        /// to projects, and wants to temporarily sacrifice correctness for performance.
        /// </summary>
        public bool SkipEvaluation
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _skipEvaluation;
                }
            }
 
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_skipEvaluation != value)
                    {
                        _skipEvaluation = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.SkipEvaluation));
                }
            }
        }
 
        /// <summary>
        /// Whether <see cref="Project.MarkDirty()">MarkDirty()</see> is temporarily disabled on
        /// projects in this collection.
        /// This allows, for example, global properties to be set without projects getting
        /// marked dirty for reevaluation as a consequence.
        /// </summary>
        public bool DisableMarkDirty
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _disableMarkDirty;
                }
            }
 
            set
            {
                bool sendEvent = false;
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_disableMarkDirty != value)
                    {
                        _disableMarkDirty = value;
                        sendEvent = true;
                    }
                }
 
                if (sendEvent)
                {
                    OnProjectCollectionChanged(
                        new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.DisableMarkDirty));
                }
            }
        }
 
        /// <summary>
        /// Global collection id.
        /// Can be used for external providers to optimize the cross-site link exchange
        /// </summary>
        internal Guid CollectionId { get; } = Guid.NewGuid();
 
        /// <summary>
        /// External project support.
        /// Establish a remote project link for this collection.
        /// </summary>
        internal ExternalProjectsProvider Link
        {
            get => _link;
            set => Interlocked.Exchange(ref _link, value)?.Disconnected(this);
        }
 
        /// <summary>
        /// Logging service that should be used for project load and for builds
        /// </summary>
        internal ILoggingService LoggingService
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _loggingService;
                }
            }
        }
 
        /// <summary>
        /// Gets default global properties for all projects in this collection.
        /// Dead copy.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> GlobalPropertiesCollection
        {
            [DebuggerStepThrough]
            get
            {
                var clone = new PropertyDictionary<ProjectPropertyInstance>();
 
                using (_locker.EnterDisposableReadLock())
                {
                    foreach (ProjectPropertyInstance property in _globalProperties)
                    {
                        clone.Set(property.DeepClone());
                    }
 
                    return clone;
                }
            }
        }
 
        /// <summary>
        /// Returns the property dictionary containing the properties representing the environment.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> EnvironmentProperties
        {
            get
            {
                // Retrieves the environment properties.
                // This is only done once, when the project collection is created. Any subsequent
                // environment changes will be ignored. Child nodes will be passed this set
                // of properties in their build parameters.
                return new PropertyDictionary<ProjectPropertyInstance>(SharedReadOnlyEnvironmentProperties);
            }
        }
 
        /// <summary>
        /// Returns a shared immutable property dictionary containing the properties representing the environment.
        /// </summary>
        internal PropertyDictionary<ProjectPropertyInstance> SharedReadOnlyEnvironmentProperties
        {
            get
            {
                // Retrieves the environment properties.
                // This is only done once, when the project collection is created. Any subsequent
                // environment changes will be ignored. Child nodes will be passed this set
                // of properties in their build parameters.
                using (_locker.EnterDisposableReadLock())
                {
                    if (_environmentProperties != null)
                    {
                        return _environmentProperties;
                    }
                }
 
                using (_locker.EnterDisposableWriteLock())
                {
                    if (_environmentProperties == null)
                    {
                        _environmentProperties = Utilities.GetEnvironmentProperties(makeReadOnly: true);
                    }
                    return _environmentProperties;
                }
            }
        }
 
        /// <summary>
        /// Returns the internal version for this object's state.
        /// Updated when toolsets change, indicating all contained projects are potentially invalid.
        /// </summary>
        internal int ToolsetsVersion
        {
            [DebuggerStepThrough]
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _toolsetsVersion;
                }
            }
        }
 
        /// <summary>
        /// The maximum number of nodes which can be started during the build
        /// </summary>
        internal int MaxNodeCount
        {
            get
            {
                using (_locker.EnterDisposableReadLock())
                {
                    return _maxNodeCount;
                }
            }
 
            set
            {
                using (_locker.EnterDisposableWriteLock())
                {
                    _maxNodeCount = value;
                }
            }
        }
 
        /// <summary>
        /// The cache of project root elements associated with this project collection.
        /// Each is associated with a specific project collection for two reasons:
        /// - To help protect one project collection from any XML edits through another one:
        /// until a reload from disk - when it's ready to accept changes - it won't see the edits;
        /// - So that the owner of this project collection can force the XML to be loaded again
        /// from disk, by doing <see cref="UnloadAllProjects"/>.
        /// </summary>
        internal ProjectRootElementCacheBase ProjectRootElementCache { get; }
 
        /// <summary>
        /// Escape a string using MSBuild escaping format. For example, "%3b" for ";".
        /// Only characters that are especially significant to MSBuild parsing are escaped.
        /// Callers can use this method to make a string safe to be parsed to other methods
        /// that would otherwise expand it; or to make a string safe to be written to a project file.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")]
        public static string Escape(string unescapedString)
        {
            return EscapingUtilities.Escape(unescapedString);
        }
 
        /// <summary>
        /// Unescape a string using MSBuild escaping format. For example, "%3b" for ";".
        /// All escaped characters are unescaped.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Public API that has shipped")]
        public static string Unescape(string escapedString)
        {
            return EscapingUtilities.UnescapeAll(escapedString);
        }
 
        /// <summary>
        /// Returns true if there is a toolset defined for the specified
        /// tools version, otherwise false.
        /// </summary>
        public bool ContainsToolset(string toolsVersion) => GetToolset(toolsVersion) != null;
 
        /// <summary>
        /// Add a new toolset.
        /// Replaces any existing toolset with the same tools version.
        /// </summary>
        public void AddToolset(Toolset toolset)
        {
            ErrorUtilities.VerifyThrowArgumentNull(toolset);
            using (_locker.EnterDisposableWriteLock())
            {
                _toolsets[toolset.ToolsVersion] = toolset;
                _toolsetsVersion++;
            }
 
            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
        }
 
        /// <summary>
        /// Remove a toolset.
        /// Returns true if it was present, otherwise false.
        /// </summary>
        public bool RemoveToolset(string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(toolsVersion);
 
            bool changed;
            using (_locker.EnterDisposableWriteLock())
            {
                changed = RemoveToolsetInternal(toolsVersion);
            }
 
            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
            }
 
            return changed;
        }
 
        /// <summary>
        /// Removes all toolsets.
        /// </summary>
        public void RemoveAllToolsets()
        {
            bool changed = false;
            using (_locker.EnterDisposableWriteLock())
            {
                var toolsets = new List<Toolset>(Toolsets);
 
                foreach (Toolset toolset in toolsets)
                {
                    changed |= RemoveToolsetInternal(toolset.ToolsVersion);
                }
            }
 
            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Toolsets));
            }
        }
 
        /// <summary>
        /// Get the toolset with the specified tools version.
        /// If it is not present, returns null.
        /// </summary>
        public Toolset GetToolset(string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(toolsVersion);
            using (_locker.EnterDisposableWriteLock())
            {
                _toolsets.TryGetValue(toolsVersion, out var toolset);
                return toolset;
            }
        }
 
        /// <summary>
        /// Figure out what ToolsVersion to use to actually build the project with.
        /// </summary>
        /// <param name="explicitToolsVersion">The user-specified ToolsVersion (through e.g. /tv: on the command line). May be null</param>
        /// <param name="toolsVersionFromProject">The ToolsVersion from the project file. May be null</param>
        /// <returns>The ToolsVersion we should use to build this project.  Should never be null.</returns>
        public string GetEffectiveToolsVersion(string explicitToolsVersion, string toolsVersionFromProject)
        {
            return Utilities.GenerateToolsVersionToUse(explicitToolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion, out _);
        }
 
        /// <summary>
        /// Returns any and all loaded projects with the provided path.
        /// There may be more than one, if they are distinguished by global properties
        /// and/or tools version.
        /// </summary>
        public ICollection<Project> GetLoadedProjects(string fullPath)
        {
            return GetLoadedProjects(true, fullPath);
        }
 
        /// <summary>
        /// Returns any and all loaded projects with the provided path.
        /// There may be more than one, if they are distinguished by global properties
        /// and/or tools version.
        /// </summary>
        internal ICollection<Project> GetLoadedProjects(bool includeExternal, string fullPath = null)
        {
            List<Project> loaded;
            using (_locker.EnterDisposableWriteLock())
            {
                loaded = fullPath == null ? new List<Project>(_loadedProjects) : new List<Project>(_loadedProjects.GetMatchingProjectsIfAny(fullPath));
            }
 
            if (includeExternal)
            {
                var link = Link;
                if (link != null)
                {
                    loaded.AddRange(link.GetLoadedProjects(fullPath));
                }
            }
 
            return loaded;
        }
 
        /// <summary>
        /// Loads a project with the specified filename, using the collection's global properties and tools version.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName)
        {
            return LoadProject(fileName, null);
        }
 
        /// <summary>
        /// Loads a project with the specified filename and tools version, using the collection's global properties.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <param name="toolsVersion">The tools version to use. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName, string toolsVersion)
        {
            return LoadProject(fileName, null /* use project collection's global properties */, toolsVersion);
        }
 
        /// <summary>
        /// Loads a project with the specified filename, tools version and global properties.
        /// If a matching project is already loaded, it will be returned, otherwise a new project will be loaded.
        /// </summary>
        /// <param name="fileName">The project file to load</param>
        /// <param name="globalProperties">The global properties to use. May be null, in which case the containing project collection's global properties will be used.</param>
        /// <param name="toolsVersion">The tools version. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(string fileName, IDictionary<string, string> globalProperties, string toolsVersion)
        {
            ErrorUtilities.VerifyThrowArgumentLength(fileName);
            fileName = FileUtilities.NormalizePath(fileName);
 
            using (_locker.EnterDisposableWriteLock())
            {
                if (globalProperties == null)
                {
                    globalProperties = GlobalProperties;
                }
                else
                {
                    // We need to update the set of global properties to merge in the ProjectCollection global properties --
                    // otherwise we might end up declaring "not matching" a project that actually does ... and then throw
                    // an exception when we go to actually add the newly created project to the ProjectCollection.
                    // BUT remember that project global properties win -- don't override a property that already exists.
                    foreach (KeyValuePair<string, string> globalProperty in GlobalProperties)
                    {
                        if (!globalProperties.ContainsKey(globalProperty.Key))
                        {
                            globalProperties.Add(globalProperty);
                        }
                    }
                }
 
                // 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 toolsVersionFromProject = null;
 
                if (toolsVersion == null)
                {
                    // Load the project XML to get any ToolsVersion attribute.
                    // If there isn't already an equivalent project loaded, the real load we'll do will be satisfied from the cache.
                    // If there is already an equivalent project loaded, we'll never need this XML -- but it'll already
                    // have been loaded by that project so it will have been satisfied from the ProjectRootElementCache.
                    // Either way, no time wasted.
                    try
                    {
                        ProjectRootElement xml = ProjectRootElement.OpenProjectOrSolution(fileName, globalProperties, toolsVersion, ProjectRootElementCache, true /*explicitlyloaded*/);
                        toolsVersionFromProject = (xml.ToolsVersion.Length > 0) ? xml.ToolsVersion : DefaultToolsVersion;
                    }
                    catch (InvalidProjectFileException ex)
                    {
                        var buildEventContext = new BuildEventContext(0 /* node ID */, BuildEventContext.InvalidTargetId, BuildEventContext.InvalidProjectContextId, BuildEventContext.InvalidTaskId);
                        LoggingService.LogInvalidProjectFileError(buildEventContext, ex);
                        throw;
                    }
                }
 
                string effectiveToolsVersion = Utilities.GenerateToolsVersionToUse(toolsVersion, toolsVersionFromProject, GetToolset, DefaultToolsVersion, out _);
                Project project = _loadedProjects.GetMatchingProjectIfAny(fileName, globalProperties, effectiveToolsVersion);
 
                if (project == null)
                {
                    // The Project constructor adds itself to our collection,
                    // it is not done by us
                    project = new Project(fileName, globalProperties, effectiveToolsVersion, this);
                }
 
                return project;
            }
        }
 
        /// <summary>
        /// Loads a project with the specified reader, using the collection's global properties and tools version.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader)
        {
            return LoadProject(xmlReader, null);
        }
 
        /// <summary>
        /// Loads a project with the specified reader and tools version, using the collection's global properties.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <param name="toolsVersion">The tools version to use. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader, string toolsVersion)
        {
            return LoadProject(xmlReader, null /* use project collection's global properties */, toolsVersion);
        }
 
        /// <summary>
        /// Loads a project with the specified reader, tools version and global properties.
        /// The project will be added to this project collection when it is named.
        /// </summary>
        /// <param name="xmlReader">Xml reader to read project from</param>
        /// <param name="globalProperties">The global properties to use. May be null in which case the containing project collection's global properties will be used.</param>
        /// <param name="toolsVersion">The tools version. May be null.</param>
        /// <returns>A loaded project.</returns>
        public Project LoadProject(XmlReader xmlReader, IDictionary<string, string> globalProperties, string toolsVersion)
        {
            return new Project(xmlReader, globalProperties, toolsVersion, this);
        }
 
        /// <summary>
        /// Adds a logger to the collection of loggers used for builds of projects in this collection.
        /// If the logger object is already in the collection, does nothing.
        /// </summary>
        public void RegisterLogger(ILogger logger)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                RegisterLoggerInternal(logger);
            }
 
            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }
 
        /// <summary>
        /// Adds some loggers to the collection of loggers used for builds of projects in this collection.
        /// If any logger object is already in the collection, does nothing for that logger.
        /// May be null.
        /// </summary>
        public void RegisterLoggers(IEnumerable<ILogger> loggers)
        {
            bool changed = false;
            if (loggers != null)
            {
                using (_locker.EnterDisposableWriteLock())
                {
                    foreach (ILogger logger in loggers)
                    {
                        RegisterLoggerInternal(logger);
                        changed = true;
                    }
                }
            }
 
            if (changed)
            {
                OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
            }
        }
 
        /// <summary>
        /// Adds some remote loggers to the collection of remote loggers used for builds of projects in this collection.
        /// May be null.
        /// </summary>
        public void RegisterForwardingLoggers(IEnumerable<ForwardingLoggerRecord> remoteLoggers)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                if (remoteLoggers != null)
                {
                    foreach (ForwardingLoggerRecord remoteLoggerRecord in remoteLoggers)
                    {
                        _loggingService.RegisterDistributedLogger(new ReusableLogger(remoteLoggerRecord.CentralLogger), remoteLoggerRecord.ForwardingLoggerDescription);
                    }
                }
            }
 
            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }
 
        /// <summary>
        /// Removes all loggers from the collection of loggers used for builds of projects in this collection.
        /// </summary>
        public void UnregisterAllLoggers()
        {
            using (_locker.EnterDisposableWriteLock())
            {
                _loggingService.UnregisterAllLoggers();
 
                // UNDONE: Logging service should not shut down when all loggers are unregistered.
                // VS unregisters all loggers on the same project collection often. To workaround this, we have to create it again now!
                CreateLoggingService(MaxNodeCount, OnlyLogCriticalEvents);
            }
 
            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.Loggers));
        }
 
        /// <summary>
        /// Unloads the specific project specified.
        /// Host should call this when they are completely done with the project.
        /// If project was not already loaded, throws InvalidOperationException.
        /// </summary>
        public void UnloadProject(Project project)
        {
            if (project.IsLinked)
            {
                project.Zombify();
                return;
            }
 
            using (_locker.EnterDisposableWriteLock())
            {
                bool existed = _loadedProjects.RemoveProject(project);
                ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded");
 
                project.Zombify();
 
                // If we've removed the last entry for the given project full path
                // then unregister any and all host objects for that project
                if (_hostServices != null && _loadedProjects.GetMatchingProjectsIfAny(project.FullPath).Count == 0)
                {
                    _hostServices.UnregisterProject(project.FullPath);
                }
 
                // Release our own cache's strong references to try to help
                // free memory. These may be the last references to the ProjectRootElements
                // in the cache, so the cache shouldn't hold strong references to them of its own.
                ProjectRootElementCache.DiscardStrongReferences();
            }
        }
 
        /// <summary>
        /// Unloads a project XML root element from the weak cache.
        /// </summary>
        /// <param name="projectRootElement">The project XML root element to unload.</param>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the project XML root element to unload is still in use by a loaded project or its imports.
        /// </exception>
        /// <remarks>
        /// This method is useful for the case where the host knows that all projects using this XML element
        /// are unloaded, and desires to discard any unsaved changes.
        /// </remarks>
        public void UnloadProject(ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElement);
            if (projectRootElement.Link != null)
            {
                return;
            }
 
            using (_locker.EnterDisposableWriteLock())
            {
                Project conflictingProject = GetLoadedProjects(false, null).FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement));
                if (conflictingProject != null)
                {
                    ErrorUtilities.ThrowInvalidOperation("OM_ProjectXmlCannotBeUnloadedDueToLoadedProjects", projectRootElement.FullPath, conflictingProject.FullPath);
                }
 
                ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement);
            }
        }
 
        /// <summary>
        /// Unloads all the projects contained by this ProjectCollection.
        /// Host should call this when they are completely done with all the projects.
        /// </summary>
        public void UnloadAllProjects()
        {
            using (_locker.EnterDisposableWriteLock())
            {
                foreach (Project project in _loadedProjects)
                {
                    project.Zombify();
 
                    // We're removing every entry from the project collection
                    // so unregister any and all host objects for each project
                    _hostServices?.UnregisterProject(project.FullPath);
                }
 
                _loadedProjects.RemoveAllProjects();
 
                ProjectRootElementCache.Clear();
            }
        }
 
        /// <summary>
        /// Get any global property on the project collection that has the specified name,
        /// otherwise returns null.
        /// </summary>
        public ProjectPropertyInstance GetGlobalProperty(string name)
        {
            using (_locker.EnterDisposableReadLock())
            {
                return _globalProperties[name];
            }
        }
 
        /// <summary>
        /// Set a global property at the collection-level,
        /// and on all projects in the project collection.
        /// </summary>
        public void SetGlobalProperty(string name, string value)
        {
            bool sendEvent = false;
            using (_locker.EnterDisposableWriteLock())
            {
                ProjectPropertyInstance propertyInGlobalProperties = _globalProperties.GetProperty(name);
                bool changed = propertyInGlobalProperties == null || !String.Equals(((IValued)propertyInGlobalProperties).EscapedValue, value, StringComparison.OrdinalIgnoreCase);
                if (changed)
                {
                    _globalProperties.Set(ProjectPropertyInstance.Create(name, value));
                    sendEvent = true;
                }
 
                // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add
                var projects = new List<Project>(_loadedProjects);
                foreach (Project project in projects)
                {
                    project.SetGlobalProperty(name, value);
                }
            }
 
            if (sendEvent)
            {
                OnProjectCollectionChanged(
                    new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties));
            }
        }
 
        /// <summary>
        /// Removes a global property from the collection-level set of global properties,
        /// and all projects in the project collection.
        /// If it was on this project collection, returns true.
        /// </summary>
        public bool RemoveGlobalProperty(string name)
        {
            bool set;
            using (_locker.EnterDisposableWriteLock())
            {
                set = _globalProperties.Remove(name);
 
                // Copy LoadedProjectCollection as modifying a project's global properties will cause it to re-add
                var projects = new List<Project>(_loadedProjects);
                foreach (Project project in projects)
                {
                    project.RemoveGlobalProperty(name);
                }
            }
 
            OnProjectCollectionChanged(new ProjectCollectionChangedEventArgs(ProjectCollectionChangedState.GlobalProperties));
 
            return set;
        }
 
        /// <summary>
        /// Called when a host is completely done with the project collection.
        /// </summary>
        // UNDONE: This is a hack to make sure the logging thread shuts down if the build used the logging service
        // off the ProjectCollection. After CTP we need to rationalize this and see if we can remove the logging service from
        // the project collection entirely so this isn't necessary.
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        #region IBuildComponent Members
 
        /// <summary>
        /// Initializes the component with the component host.
        /// </summary>
        /// <param name="host">The component host.</param>
        void IBuildComponent.InitializeComponent(IBuildComponentHost host)
        {
        }
 
        /// <summary>
        /// Shuts down the component.
        /// </summary>
        void IBuildComponent.ShutdownComponent()
        {
        }
 
        #endregion
 
        /// <summary>
        /// Unloads a project XML root element from the cache entirely, if it is not
        /// in use by project loaded into this collection.
        /// Returns true if it was unloaded successfully, or was not already loaded.
        /// Returns false if it was not unloaded because it was still in use by a loaded <see cref="Project"/>.
        /// </summary>
        /// <param name="projectRootElement">The project XML root element to unload.</param>
        public bool TryUnloadProject(ProjectRootElement projectRootElement)
        {
            ErrorUtilities.VerifyThrowArgumentNull(projectRootElement);
            if (projectRootElement.Link != null)
            {
                return false;
            }
 
            using (_locker.EnterDisposableWriteLock())
            {
                ProjectRootElementCache.DiscardStrongReferences();
 
                Project conflictingProject = GetLoadedProjects(false, null).FirstOrDefault(project => project.UsesProjectRootElement(projectRootElement));
                if (conflictingProject == null)
                {
                    ProjectRootElementCache.DiscardAnyWeakReference(projectRootElement);
                    return true;
                }
 
                return false;
            }
        }
 
        /// <summary>
        /// Logs a BuildFinished event. This is used specifically when a ProjectCollection is created but never actually built, yet a BuildFinished event
        /// is still desired. As an example, if a Project is just meant to be evaluated, but a binlog is also collected, that binlog should be able to
        /// say the build succeeded or failed. This provides a mechanism to achieve that.
        /// </summary>
        public void LogBuildFinishedEvent(bool success) => _loggingService.LogBuildFinished(success);
 
        /// <summary>
        /// Called by a Project object to load itself into this collection.
        /// If the project was already loaded under a different name, it is unloaded.
        /// Stores the project in the list of loaded projects if it has a name.
        /// Does not store the project if it has no name because it has not been saved to disk yet.
        /// If the project previously had a name, but was not in the collection already, throws InvalidOperationException.
        /// If the project was not previously in the collection, sets the collection's global properties on it.
        /// </summary>
        internal void OnAfterRenameLoadedProject(string oldFullPathIfAny, Project project)
        {
            if (project.FullPath == null)
            {
                return;
            }
 
            using (_locker.EnterDisposableWriteLock())
            {
                if (oldFullPathIfAny != null)
                {
                    bool existed = _loadedProjects.RemoveProject(oldFullPathIfAny, project);
                    ErrorUtilities.VerifyThrowInvalidOperation(existed, "OM_ProjectWasNotLoaded");
                }
 
                // The only time this ever gets called with a null full path is when the project is first being
                // constructed.  The mere fact that this method is being called means that this project will belong
                // to this project collection.  As such, it has already had all necessary global properties applied
                // when being constructed -- we don't need to do anything special here.
                // If we did add global properties here, we would just end up either duplicating work or possibly
                // wiping out global properties set on the project meant to override the ProjectCollection copies.
                _loadedProjects.AddProject(project);
 
                if (_hostServices != null)
                {
                    HostServices.OnRenameProject(oldFullPathIfAny, project.FullPath);
                }
            }
        }
 
        /// <summary>
        /// Called after a loaded project's global properties are changed, so we can update
        /// our loaded project table.
        /// Project need not already be in the project collection yet, but it can't be in another one.
        /// </summary>
        /// <remarks>
        /// We have to remove and re-add so that there's an error if there's already an equivalent
        /// project loaded.
        /// </remarks>
        internal void AfterUpdateLoadedProjectGlobalProperties(Project project)
        {
            using (_locker.EnterDisposableWriteLock())
            {
                ErrorUtilities.VerifyThrowInvalidOperation(ReferenceEquals(project.ProjectCollection, this), "OM_IncorrectObjectAssociation", "Project", "ProjectCollection");
 
                if (project.FullPath == null)
                {
                    return;
                }
 
                bool existed = _loadedProjects.RemoveProject(project);
                if (existed)
                {
                    _loadedProjects.AddProject(project);
                }
            }
        }
 
        /// <summary>
        /// Following standard framework guideline dispose pattern.
        /// Shut down logging service if the project collection owns one, in order
        /// to shut down the logger thread and loggers.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources..</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                ShutDownLoggingService();
                if (ProjectRootElementCache != null)
                {
                    ProjectRootElementCache.ProjectRootElementAddedHandler -= ProjectRootElementCache_ProjectRootElementAddedHandler;
                    ProjectRootElementCache.ProjectRootElementDirtied -= ProjectRootElementCache_ProjectRootElementDirtiedHandler;
                    ProjectRootElementCache.ProjectDirtied -= ProjectRootElementCache_ProjectDirtiedHandler;
                }
                Tracing.Dump();
            }
        }
 
        /// <summary>
        /// Remove a toolset and does not raise events. The caller should have acquired a write lock on this method's behalf.
        /// </summary>
        /// <param name="toolsVersion">The toolset to remove.</param>
        /// <returns><c>true</c> if the toolset was found and removed; <c>false</c> otherwise.</returns>
        private bool RemoveToolsetInternal(string toolsVersion)
        {
            Debug.Assert(_locker.IsWriteLockHeld);
 
            if (!_toolsets.Remove(toolsVersion))
            {
                return false;
            }
 
            _toolsetsVersion++;
            return true;
        }
 
        /// <summary>
        /// Adds a logger to the collection of loggers used for builds of projects in this collection.
        /// If the logger object is already in the collection, does nothing.
        /// </summary>
        private void RegisterLoggerInternal(ILogger logger)
        {
            ErrorUtilities.VerifyThrowArgumentNull(logger);
            Debug.Assert(_locker.IsWriteLockHeld);
            _loggingService.RegisterLogger(new ReusableLogger(logger));
        }
 
        /// <summary>
        /// Handler which is called when a project is added to the RootElementCache of this project collection. We then fire an event indicating that a project was added to the collection itself.
        /// </summary>
        private void ProjectRootElementCache_ProjectRootElementAddedHandler(object sender, ProjectRootElementCacheAddEntryEventArgs e)
        {
            ProjectAdded?.Invoke(this, new ProjectAddedToProjectCollectionEventArgs(e.RootElement));
        }
 
        /// <summary>
        /// Handler which is called when a project that is part of this collection is dirtied. We then fire an event indicating that a project has been dirtied.
        /// </summary>
        private void ProjectRootElementCache_ProjectRootElementDirtiedHandler(object sender, ProjectXmlChangedEventArgs e)
        {
            OnProjectXmlChanged(e);
        }
 
        /// <summary>
        /// Handler which is called when a project is dirtied.
        /// </summary>
        private void ProjectRootElementCache_ProjectDirtiedHandler(object sender, ProjectChangedEventArgs e)
        {
            OnProjectChanged(e);
        }
 
        /// <summary>
        /// Raises the <see cref="ProjectXmlChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate ProjectRootElement-specific details.</param>
        private void OnProjectXmlChanged(ProjectXmlChangedEventArgs e)
        {
            ProjectXmlChanged?.Invoke(this, e);
        }
 
        /// <summary>
        /// Raises the <see cref="ProjectChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate Project-specific details.</param>
        private void OnProjectChanged(ProjectChangedEventArgs e)
        {
            ProjectChanged?.Invoke(this, e);
        }
 
        /// <summary>
        /// Raises the <see cref="ProjectCollectionChanged"/> event.
        /// </summary>
        /// <param name="e">The event arguments that indicate details on what changed on the collection.</param>
        private void OnProjectCollectionChanged(ProjectCollectionChangedEventArgs e)
        {
            Debug.Assert(!_locker.IsWriteLockHeld, "We should never raise events while holding a private lock.");
            ProjectCollectionChanged?.Invoke(this, e);
        }
 
        /// <summary>
        /// Shutdown the logging service
        /// </summary>
        private void ShutDownLoggingService()
        {
            if (_loggingService != null)
            {
                try
                {
                    (LoggingService as LoggingService)?.WaitForLoggingToProcessEvents();
                    ((IBuildComponent)LoggingService).ShutdownComponent();
                }
                catch (LoggerException)
                {
                    throw;
                }
                catch (InternalLoggerException)
                {
                    throw;
                }
                catch (Exception ex)
                {
                    // According to Framework Guidelines, Dispose methods should never throw except in dire circumstances.
                    // However if we throw at all, its a bug. Throw InternalErrorException to emphasize that.
                    ErrorUtilities.ThrowInternalError("Throwing from logger shutdown", ex);
                    throw;
                }
 
                _loggingService = null;
            }
        }
 
        /// <summary>
        /// Create a new logging service
        /// </summary>
        private void CreateLoggingService(int maxCPUCount, bool onlyLogCriticalEvents)
        {
            _loggingService = BackEnd.Logging.LoggingService.CreateLoggingService(_loggerMode, 0 /*Evaluation can be done as if it was on node "0"*/);
            _loggingService.MaxCPUCount = maxCPUCount;
            _loggingService.OnlyLogCriticalEvents = onlyLogCriticalEvents;
        }
 
        /// <summary>
        /// Reset the toolsets using the provided toolset reader, used by unit tests
        /// </summary>
        internal void ResetToolsetsForTests(ToolsetConfigurationReader configurationReaderForTestsOnly)
        {
            InitializeToolsetCollection(configReader: configurationReaderForTestsOnly);
        }
 
#if FEATURE_WIN32_REGISTRY
        /// <summary>
        /// Reset the toolsets using the provided toolset reader, used by unit tests
        /// </summary>
        internal void ResetToolsetsForTests(ToolsetRegistryReader registryReaderForTestsOnly)
        {
            InitializeToolsetCollection(registryReader: registryReaderForTestsOnly);
        }
#endif
 
        /// <summary>
        /// Populate Toolsets with a dictionary of (toolset version, Toolset)
        /// using information from the registry and config file, if any.
        /// </summary>
        private void InitializeToolsetCollection(
#if FEATURE_WIN32_REGISTRY
                ToolsetRegistryReader registryReader = null,
#endif
                ToolsetConfigurationReader configReader = null)
        {
            _toolsets = new Dictionary<string, Toolset>(StringComparer.OrdinalIgnoreCase);
 
            // We only want our local toolset (as defined in MSBuild.exe.config) when we're operating locally...
            _defaultToolsVersion = ToolsetReader.ReadAllToolsets(_toolsets,
#if FEATURE_WIN32_REGISTRY
                    registryReader,
#endif
                    configReader,
                    EnvironmentProperties, _globalProperties, ToolsetLocations);
 
            _toolsetsVersion++;
        }
 
        /// <summary>
        /// Event to provide information about what project just got added to the project collection.
        /// </summary>
        [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "This has been API reviewed")]
        public class ProjectAddedToProjectCollectionEventArgs : EventArgs
        {
            /// <summary>
            /// The root element which was added to the project collection.
            /// </summary>
            public ProjectAddedToProjectCollectionEventArgs(ProjectRootElement element)
            {
                ProjectRootElement = element;
            }
 
            /// <summary>
            /// Root element which was added to the project collection.
            /// </summary>
            public ProjectRootElement ProjectRootElement { get; }
        }
 
        /// <summary>
        /// The ReusableLogger wraps a logger and allows it to be used for both design-time and build-time.  It internally swaps
        /// between the design-time and build-time event sources in response to Initialize and Shutdown events.
        /// </summary>
        internal class ReusableLogger : INodeLogger, IEventSource4
        {
            /// <summary>
            /// The logger we are wrapping.
            /// </summary>
            private readonly ILogger _originalLogger;
 
            /// <summary>
            /// Returns the logger we are wrapping.
            /// </summary>
            internal ILogger OriginalLogger => _originalLogger;
 
            /// <summary>
            /// The design-time event source
            /// </summary>
            private IEventSource _designTimeEventSource;
 
            /// <summary>
            /// The build-time event source
            /// </summary>
            private IEventSource _buildTimeEventSource;
 
            /// <summary>
            /// The Any event handler
            /// </summary>
            private AnyEventHandler _anyEventHandler;
 
            /// <summary>
            /// The BuildFinished event handler
            /// </summary>
            private BuildFinishedEventHandler _buildFinishedEventHandler;
 
            /// <summary>
            /// The BuildStarted event handler
            /// </summary>
            private BuildStartedEventHandler _buildStartedEventHandler;
 
            /// <summary>
            /// The Custom event handler
            /// </summary>
            private CustomBuildEventHandler _customBuildEventHandler;
 
            /// <summary>
            /// The Error event handler
            /// </summary>
            private BuildErrorEventHandler _buildErrorEventHandler;
 
            /// <summary>
            /// The Message event handler
            /// </summary>
            private BuildMessageEventHandler _buildMessageEventHandler;
 
            /// <summary>
            /// The ProjectFinished event handler
            /// </summary>
            private ProjectFinishedEventHandler _projectFinishedEventHandler;
 
            /// <summary>
            /// The ProjectStarted event handler
            /// </summary>
            private ProjectStartedEventHandler _projectStartedEventHandler;
 
            /// <summary>
            /// The Status event handler
            /// </summary>
            private BuildStatusEventHandler _buildStatusEventHandler;
 
            /// <summary>
            /// The TargetFinished event handler
            /// </summary>
            private TargetFinishedEventHandler _targetFinishedEventHandler;
 
            /// <summary>
            /// The TargetStarted event handler
            /// </summary>
            private TargetStartedEventHandler _targetStartedEventHandler;
 
            /// <summary>
            /// The TaskFinished event handler
            /// </summary>
            private TaskFinishedEventHandler _taskFinishedEventHandler;
 
            /// <summary>
            /// The TaskStarted event handler
            /// </summary>
            private TaskStartedEventHandler _taskStartedEventHandler;
 
            /// <summary>
            /// The Warning event handler
            /// </summary>
            private BuildWarningEventHandler _buildWarningEventHandler;
 
            /// <summary>
            ///  The telemetry event handler.
            /// </summary>
            private TelemetryEventHandler _telemetryEventHandler;
 
            private bool _includeEvaluationMetaprojects;
 
            private bool _includeEvaluationProfiles;
 
            private bool _includeTaskInputs;
 
            private bool _includeEvaluationPropertiesAndItems;
 
            /// <summary>
            /// Constructor.
            /// </summary>
            public ReusableLogger(ILogger originalLogger)
            {
                ErrorUtilities.VerifyThrowArgumentNull(originalLogger);
                _originalLogger = originalLogger;
            }
 
            #region IEventSource Members
 
            /// <summary>
            /// The Message logging event
            /// </summary>
            public event BuildMessageEventHandler MessageRaised;
 
            /// <summary>
            /// The Error logging event
            /// </summary>
            public event BuildErrorEventHandler ErrorRaised;
 
            /// <summary>
            /// The Warning logging event
            /// </summary>
            public event BuildWarningEventHandler WarningRaised;
 
            /// <summary>
            /// The BuildStarted logging event
            /// </summary>
            public event BuildStartedEventHandler BuildStarted;
 
            /// <summary>
            /// The BuildFinished logging event
            /// </summary>
            public event BuildFinishedEventHandler BuildFinished;
 
            /// <summary>
            /// The BuildCanceled logging event
            /// </summary>
            public event BuildCanceledEventHandler BuildCanceled;
 
            /// <summary>
            /// The ProjectStarted logging event
            /// </summary>
            public event ProjectStartedEventHandler ProjectStarted;
 
            /// <summary>
            /// The ProjectFinished logging event
            /// </summary>
            public event ProjectFinishedEventHandler ProjectFinished;
 
            /// <summary>
            /// The TargetStarted logging event
            /// </summary>
            public event TargetStartedEventHandler TargetStarted;
 
            /// <summary>
            /// The TargetFinished logging event
            /// </summary>
            public event TargetFinishedEventHandler TargetFinished;
 
            /// <summary>
            /// The TashStarted logging event
            /// </summary>
            public event TaskStartedEventHandler TaskStarted;
 
            /// <summary>
            /// The TaskFinished logging event
            /// </summary>
            public event TaskFinishedEventHandler TaskFinished;
 
            /// <summary>
            /// The Custom logging event
            /// </summary>
            public event CustomBuildEventHandler CustomEventRaised;
 
            /// <summary>
            /// The Status logging event
            /// </summary>
            public event BuildStatusEventHandler StatusEventRaised;
 
            /// <summary>
            /// The Any logging event
            /// </summary>
            public event AnyEventHandler AnyEventRaised;
 
            /// <summary>
            /// The telemetry sent event.
            /// </summary>
            public event TelemetryEventHandler TelemetryLogged;
 
            /// <summary>
            /// Should evaluation events include generated metaprojects?
            /// </summary>
            public void IncludeEvaluationMetaprojects()
            {
                if (_buildTimeEventSource is IEventSource3 buildEventSource3)
                {
                    buildEventSource3.IncludeEvaluationMetaprojects();
                }
 
                if (_designTimeEventSource is IEventSource3 designTimeEventSource3)
                {
                    designTimeEventSource3.IncludeEvaluationMetaprojects();
                }
 
                _includeEvaluationMetaprojects = true;
            }
 
            /// <summary>
            /// Should evaluation events include profiling information?
            /// </summary>
            public void IncludeEvaluationProfiles()
            {
                if (_buildTimeEventSource is IEventSource3 buildEventSource3)
                {
                    buildEventSource3.IncludeEvaluationProfiles();
                }
 
                if (_designTimeEventSource is IEventSource3 designTimeEventSource3)
                {
                    designTimeEventSource3.IncludeEvaluationProfiles();
                }
 
                _includeEvaluationProfiles = true;
            }
 
            /// <summary>
            /// Should task events include task inputs?
            /// </summary>
            public void IncludeTaskInputs()
            {
                if (_buildTimeEventSource is IEventSource3 buildEventSource3)
                {
                    buildEventSource3.IncludeTaskInputs();
                }
 
                if (_designTimeEventSource is IEventSource3 designTimeEventSource3)
                {
                    designTimeEventSource3.IncludeTaskInputs();
                }
 
                _includeTaskInputs = true;
            }
 
            public void IncludeEvaluationPropertiesAndItems()
            {
                if (_buildTimeEventSource is IEventSource4 buildEventSource4)
                {
                    buildEventSource4.IncludeEvaluationPropertiesAndItems();
                }
 
                if (_designTimeEventSource is IEventSource4 designTimeEventSource4)
                {
                    designTimeEventSource4.IncludeEvaluationPropertiesAndItems();
                }
 
                _includeEvaluationPropertiesAndItems = true;
            }
 
            #endregion
 
            #region ILogger Members
 
            /// <summary>
            /// The logger verbosity
            /// </summary>
            public LoggerVerbosity Verbosity
            {
                get => _originalLogger.Verbosity;
                set => _originalLogger.Verbosity = value;
            }
 
            /// <summary>
            /// The logger parameters
            /// </summary>
            public string Parameters
            {
                get => _originalLogger.Parameters;
 
                set => _originalLogger.Parameters = value;
            }
 
            /// <summary>
            /// If we haven't yet been initialized, we register for design time events and initialize the logger we are holding.
            /// If we are in design-time mode
            /// </summary>
            public void Initialize(IEventSource eventSource, int nodeCount)
            {
                if (_designTimeEventSource == null)
                {
                    _designTimeEventSource = eventSource;
                    RegisterForEvents(_designTimeEventSource);
 
                    if (_originalLogger is INodeLogger logger)
                    {
                        logger.Initialize(this, nodeCount);
                    }
                    else
                    {
                        _originalLogger.Initialize(this);
                    }
                }
                else
                {
                    ErrorUtilities.VerifyThrow(_buildTimeEventSource == null, "Already registered for build-time.");
                    _buildTimeEventSource = eventSource;
                    UnregisterForEvents(_designTimeEventSource);
                    RegisterForEvents(_buildTimeEventSource);
                }
            }
 
            /// <summary>
            /// If we haven't yet been initialized, we register for design time events and initialize the logger we are holding.
            /// If we are in design-time mode
            /// </summary>
            public void Initialize(IEventSource eventSource)
            {
                Initialize(eventSource, 1);
            }
 
            /// <summary>
            /// If we are in build-time mode, we unregister for build-time events and re-register for design-time events.
            /// If we are in design-time mode, we unregister for design-time events and shut down the logger we are holding.
            /// </summary>
            public void Shutdown()
            {
                if (_buildTimeEventSource != null)
                {
                    UnregisterForEvents(_buildTimeEventSource);
                    RegisterForEvents(_designTimeEventSource);
                    _buildTimeEventSource = null;
                }
                else
                {
                    ErrorUtilities.VerifyThrow(_designTimeEventSource != null, "Already unregistered for design-time.");
                    UnregisterForEvents(_designTimeEventSource);
                    _originalLogger.Shutdown();
                }
            }
 
            #endregion
 
            /// <summary>
            /// Registers for all of the events on the specified event source.
            /// </summary>
            private void RegisterForEvents(IEventSource eventSource)
            {
                // Create the handlers.
                _anyEventHandler = AnyEventRaisedHandler;
                _buildFinishedEventHandler = BuildFinishedHandler;
                _buildStartedEventHandler = BuildStartedHandler;
                _customBuildEventHandler = CustomEventRaisedHandler;
                _buildErrorEventHandler = ErrorRaisedHandler;
                _buildMessageEventHandler = MessageRaisedHandler;
                _projectFinishedEventHandler = ProjectFinishedHandler;
                _projectStartedEventHandler = ProjectStartedHandler;
                _buildStatusEventHandler = StatusEventRaisedHandler;
                _targetFinishedEventHandler = TargetFinishedHandler;
                _targetStartedEventHandler = TargetStartedHandler;
                _taskFinishedEventHandler = TaskFinishedHandler;
                _taskStartedEventHandler = TaskStartedHandler;
                _buildWarningEventHandler = WarningRaisedHandler;
                _telemetryEventHandler = TelemetryLoggedHandler;
 
                // Register for the events.
                eventSource.AnyEventRaised += _anyEventHandler;
                eventSource.BuildFinished += _buildFinishedEventHandler;
                eventSource.BuildStarted += _buildStartedEventHandler;
                eventSource.CustomEventRaised += _customBuildEventHandler;
                eventSource.ErrorRaised += _buildErrorEventHandler;
                eventSource.MessageRaised += _buildMessageEventHandler;
                eventSource.ProjectFinished += _projectFinishedEventHandler;
                eventSource.ProjectStarted += _projectStartedEventHandler;
                eventSource.StatusEventRaised += _buildStatusEventHandler;
                eventSource.TargetFinished += _targetFinishedEventHandler;
                eventSource.TargetStarted += _targetStartedEventHandler;
                eventSource.TaskFinished += _taskFinishedEventHandler;
                eventSource.TaskStarted += _taskStartedEventHandler;
                eventSource.WarningRaised += _buildWarningEventHandler;
 
                if (eventSource is IEventSource2 eventSource2)
                {
                    eventSource2.TelemetryLogged += _telemetryEventHandler;
                }
 
                if (eventSource is IEventSource3 eventSource3)
                {
                    if (_includeEvaluationMetaprojects)
                    {
                        eventSource3.IncludeEvaluationMetaprojects();
                    }
 
                    if (_includeEvaluationProfiles)
                    {
                        eventSource3.IncludeEvaluationProfiles();
                    }
 
                    if (_includeTaskInputs)
                    {
                        eventSource3.IncludeTaskInputs();
                    }
                }
 
                if (eventSource is IEventSource4 eventSource4)
                {
                    if (_includeEvaluationPropertiesAndItems)
                    {
                        eventSource4.IncludeEvaluationPropertiesAndItems();
                    }
                }
            }
 
            /// <summary>
            /// Unregisters for all events on the specified event source.
            /// </summary>
            private void UnregisterForEvents(IEventSource eventSource)
            {
                // Unregister for the events.
                eventSource.AnyEventRaised -= _anyEventHandler;
                eventSource.BuildFinished -= _buildFinishedEventHandler;
                eventSource.BuildStarted -= _buildStartedEventHandler;
                eventSource.CustomEventRaised -= _customBuildEventHandler;
                eventSource.ErrorRaised -= _buildErrorEventHandler;
                eventSource.MessageRaised -= _buildMessageEventHandler;
                eventSource.ProjectFinished -= _projectFinishedEventHandler;
                eventSource.ProjectStarted -= _projectStartedEventHandler;
                eventSource.StatusEventRaised -= _buildStatusEventHandler;
                eventSource.TargetFinished -= _targetFinishedEventHandler;
                eventSource.TargetStarted -= _targetStartedEventHandler;
                eventSource.TaskFinished -= _taskFinishedEventHandler;
                eventSource.TaskStarted -= _taskStartedEventHandler;
                eventSource.WarningRaised -= _buildWarningEventHandler;
 
                if (eventSource is IEventSource2 eventSource2)
                {
                    eventSource2.TelemetryLogged -= _telemetryEventHandler;
                }
 
                // Null out the handlers.
                _anyEventHandler = null;
                _buildFinishedEventHandler = null;
                _buildStartedEventHandler = null;
                _customBuildEventHandler = null;
                _buildErrorEventHandler = null;
                _buildMessageEventHandler = null;
                _projectFinishedEventHandler = null;
                _projectStartedEventHandler = null;
                _buildStatusEventHandler = null;
                _targetFinishedEventHandler = null;
                _targetStartedEventHandler = null;
                _taskFinishedEventHandler = null;
                _taskStartedEventHandler = null;
                _buildWarningEventHandler = null;
                _telemetryEventHandler = null;
            }
 
            /// <summary>
            /// Handler for Warning events.
            /// </summary>
            private void WarningRaisedHandler(object sender, BuildWarningEventArgs e)
            {
                WarningRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for TaskStarted events.
            /// </summary>
            private void TaskStartedHandler(object sender, TaskStartedEventArgs e)
            {
                TaskStarted?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for TaskFinished events.
            /// </summary>
            private void TaskFinishedHandler(object sender, TaskFinishedEventArgs e)
            {
                TaskFinished?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for TargetStarted events.
            /// </summary>
            private void TargetStartedHandler(object sender, TargetStartedEventArgs e)
            {
                TargetStarted?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for TargetFinished events.
            /// </summary>
            private void TargetFinishedHandler(object sender, TargetFinishedEventArgs e)
            {
                TargetFinished?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for Status events.
            /// </summary>
            private void StatusEventRaisedHandler(object sender, BuildStatusEventArgs e)
            {
                StatusEventRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for ProjectStarted events.
            /// </summary>
            private void ProjectStartedHandler(object sender, ProjectStartedEventArgs e)
            {
                ProjectStarted?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for ProjectFinished events.
            /// </summary>
            private void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs e)
            {
                ProjectFinished?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for Message events.
            /// </summary>
            private void MessageRaisedHandler(object sender, BuildMessageEventArgs e)
            {
                MessageRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for Error events.
            /// </summary>
            private void ErrorRaisedHandler(object sender, BuildErrorEventArgs e)
            {
                ErrorRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for Custom events.
            /// </summary>
            private void CustomEventRaisedHandler(object sender, CustomBuildEventArgs e)
            {
                CustomEventRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for BuildStarted events.
            /// </summary>
            private void BuildStartedHandler(object sender, BuildStartedEventArgs e)
            {
                BuildStarted?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for BuildFinished events.
            /// </summary>
            private void BuildFinishedHandler(object sender, BuildFinishedEventArgs e)
            {
                BuildFinished?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for BuildCanceled events.
            /// </summary>
            private void BuildCanceledHandler(object sender, BuildCanceledEventArgs e)
            {
                BuildCanceled?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for Any events.
            /// </summary>
            private void AnyEventRaisedHandler(object sender, BuildEventArgs e)
            {
                AnyEventRaised?.Invoke(sender, e);
            }
 
            /// <summary>
            /// Handler for telemetry events.
            /// </summary>
            private void TelemetryLoggedHandler(object sender, TelemetryEventArgs e)
            {
                TelemetryLogged?.Invoke(sender, e);
            }
        }
 
        /// <summary>
        /// Holder for the projects loaded into this collection.
        /// </summary>
        private class LoadedProjectCollection : IEnumerable<Project>
        {
            /// <summary>
            /// The collection of all projects already loaded into this collection.
            /// Key is the full path to the project, value is a list of projects with that path, each
            /// with different global properties and/or tools version.
            /// </summary>
            /// <remarks>
            /// If hosts tend to load lots of projects with the same path, the value will have to be
            /// changed to a more efficient type of collection.
            ///
            /// Lock on this object. Concurrent load must be thread safe.
            /// Not using ConcurrentDictionary because some of the add/update
            /// semantics would get convoluted.
            /// </remarks>
            private Dictionary<string, List<Project>> _loadedProjects = new Dictionary<string, List<Project>>(StringComparer.OrdinalIgnoreCase);
 
            /// <summary>
            /// Count of loaded projects
            /// </summary>
            private int _count;
 
            /// <summary>
            /// Returns the number of projects currently loaded
            /// </summary>
            internal int Count
            {
                get
                {
                    lock (_loadedProjects)
                    {
                        return _count;
                    }
                }
            }
 
            /// <summary>
            /// Enumerate all the projects
            /// </summary>
            public IEnumerator<Project> GetEnumerator()
            {
                lock (_loadedProjects)
                {
                    var projects = new List<Project>();
 
                    foreach (List<Project> projectList in _loadedProjects.Values)
                    {
                        foreach (Project project in projectList)
                        {
                            projects.Add(project);
                        }
                    }
 
                    return projects.GetEnumerator();
                }
            }
 
            /// <summary>
            /// Enumerate all the projects.
            /// </summary>
            IEnumerator IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }
 
            /// <summary>
            /// Get all projects with the provided path.
            /// Returns an empty list if there are none.
            /// </summary>
            internal IList<Project> GetMatchingProjectsIfAny(string fullPath)
            {
                lock (_loadedProjects)
                {
                    _loadedProjects.TryGetValue(fullPath, out List<Project> candidates);
 
                    return candidates ?? (IList<Project>)Array.Empty<Project>();
                }
            }
 
            /// <summary>
            /// Returns the project in the collection matching the path, global properties, and tools version provided.
            /// There can be no more than one match.
            /// If none is found, returns null.
            /// </summary>
            internal Project GetMatchingProjectIfAny(string fullPath, IDictionary<string, string> globalProperties, string toolsVersion)
            {
                lock (_loadedProjects)
                {
                    if (_loadedProjects.TryGetValue(fullPath, out List<Project> candidates))
                    {
                        foreach (Project candidate in candidates)
                        {
                            if (HasEquivalentGlobalPropertiesAndToolsVersion(candidate, globalProperties, toolsVersion))
                            {
                                return candidate;
                            }
                        }
                    }
 
                    return null;
                }
            }
 
            /// <summary>
            /// Adds the provided project to the collection.
            /// If there is already an equivalent project, throws InvalidOperationException.
            /// </summary>
            internal void AddProject(Project project)
            {
                lock (_loadedProjects)
                {
                    if (!_loadedProjects.TryGetValue(project.FullPath, out List<Project> projectList))
                    {
                        projectList = new List<Project>();
                        _loadedProjects.Add(project.FullPath, projectList);
                    }
 
                    foreach (Project existing in projectList)
                    {
                        if (HasEquivalentGlobalPropertiesAndToolsVersion(existing, project.GlobalProperties, project.ToolsVersion))
                        {
                            ErrorUtilities.ThrowInvalidOperation("OM_MatchingProjectAlreadyInCollection", existing.FullPath);
                        }
                    }
 
                    projectList.Add(project);
                    _count++;
                }
            }
 
            /// <summary>
            /// Removes the provided project from the collection.
            /// If project was not loaded, returns false.
            /// </summary>
            internal bool RemoveProject(Project project)
            {
                return RemoveProject(project.FullPath, project);
            }
 
            /// <summary>
            /// Removes a project, using the specified full path to use as the key to find it.
            /// This is specified separately in case the project was previously stored under a different path.
            /// </summary>
            internal bool RemoveProject(string projectFullPath, Project project)
            {
                lock (_loadedProjects)
                {
                    if (!_loadedProjects.TryGetValue(projectFullPath, out List<Project> projectList))
                    {
                        return false;
                    }
 
                    bool result = projectList.Remove(project);
 
                    if (result)
                    {
                        _count--;
 
                        if (projectList.Count == 0)
                        {
                            _loadedProjects.Remove(projectFullPath);
                        }
                    }
 
                    return result;
                }
            }
 
            /// <summary>
            /// Removes all projects from the collection.
            /// </summary>
            internal void RemoveAllProjects()
            {
                lock (_loadedProjects)
                {
                    _loadedProjects = new Dictionary<string, List<Project>>(StringComparer.OrdinalIgnoreCase);
                    _count = 0;
                }
            }
 
            /// <summary>
            /// Returns true if the global properties and tools version provided are equivalent to
            /// those in the provided project, otherwise false.
            /// </summary>
            private static bool HasEquivalentGlobalPropertiesAndToolsVersion(Project project, IDictionary<string, string> globalProperties, string toolsVersion)
            {
                if (!String.Equals(project.ToolsVersion, toolsVersion, StringComparison.OrdinalIgnoreCase))
                {
                    return false;
                }
 
                if (project.GlobalPropertiesCount != globalProperties.Count)
                {
                    return false;
                }
 
                foreach (KeyValuePair<string, string> leftProperty in project.GlobalPropertiesEnumerable)
                {
                    if (!globalProperties.TryGetValue(leftProperty.Key, out var rightValue))
                    {
                        return false;
                    }
 
                    if (!String.Equals(leftProperty.Value, rightValue, StringComparison.OrdinalIgnoreCase))
                    {
                        return false;
                    }
                }
 
                return true;
            }
        }
    }
}