File: BackEnd\Components\ProjectCache\ProjectCacheService.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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Construction;
using Microsoft.Build.Eventing;
using Microsoft.Build.Execution;
using Microsoft.Build.FileAccesses;
using Microsoft.Build.FileSystem;
using Microsoft.Build.Framework;
using Microsoft.Build.Graph;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental.ProjectCache
{
    internal record CacheRequest(BuildSubmission Submission, BuildRequestConfiguration Configuration);
 
    internal sealed class ProjectCacheService : IAsyncDisposable
    {
        private static readonly ParallelOptions s_parallelOptions = new() { MaxDegreeOfParallelism = Environment.ProcessorCount };
 
        private static HashSet<string> s_projectSpecificPropertyNames = new(StringComparer.OrdinalIgnoreCase) { "TargetFramework", "Configuration", "Platform", "TargetPlatform", "OutputType" };
 
        private readonly BuildManager _buildManager;
        private readonly IBuildComponentHost _componentHost;
        private readonly ILoggingService _loggingService;
#if FEATURE_REPORTFILEACCESSES
        private readonly IFileAccessManager _fileAccessManager;
#endif
        private readonly IConfigCache _configCache;
 
        private readonly ProjectCacheDescriptor? _globalProjectCacheDescriptor;
 
        private readonly ConcurrentDictionary<ProjectCacheDescriptor, Lazy<Task<ProjectCachePlugin>>> _projectCachePlugins = new(ProjectCacheDescriptorEqualityComparer.Instance);
 
        // Helps to avoid excessive allocation since BuildRequestConfiguration doesn't expose global properties in a way the plugins can consume (PropertyDictionary<ProjectPropertyInstance> vs IReadOnlyDictionary<string, string>).
        private readonly ConcurrentDictionary<BuildRequestConfiguration, IReadOnlyDictionary<string, string>> _globalPropertiesPerConfiguration = new();
 
        private bool _isVsScenario;
 
        private bool _isDisposed;
 
        private record struct ProjectCachePlugin(
            string Name,
            ProjectCachePluginBase? Instance,
#if FEATURE_REPORTFILEACCESSES
            FileAccessManager.HandlerRegistration? HandlerRegistration,
#endif
            ExceptionDispatchInfo? InitializationException = null);
 
        /// <summary>
        /// An instanatiable version of MSBuildFileSystemBase not overriding any methods,
        /// i.e. falling back to FileSystem.Default.
        /// </summary>
        private sealed class DefaultMSBuildFileSystem : MSBuildFileSystemBase
        {
            private DefaultMSBuildFileSystem()
            {
            }
 
            public static DefaultMSBuildFileSystem Instance { get; } = new();
        }
 
        public ProjectCacheService(
            BuildManager buildManager,
            ILoggingService loggingService,
#if FEATURE_REPORTFILEACCESSES
            IFileAccessManager fileAccessManager,
#endif
            IConfigCache configCache,
            ProjectCacheDescriptor? globalProjectCacheDescriptor)
        {
            _buildManager = buildManager;
            _componentHost = buildManager;
            _loggingService = loggingService;
#if FEATURE_REPORTFILEACCESSES
            _fileAccessManager = fileAccessManager;
#endif
            _configCache = configCache;
            _globalProjectCacheDescriptor = globalProjectCacheDescriptor;
        }
 
        /// <summary>
        /// Optimization which frontloads plugin initialization since we have an entire graph.
        /// </summary>
        public void InitializePluginsForGraph(
            ProjectGraph projectGraph,
            ICollection<string> requestedTargets,
            CancellationToken cancellationToken)
        {
            EnsureNotDisposed();
 
            // Performing this in a Task.Run to break away from the main thread and prevent hangs
            Task.Run(
                () =>
                {
                    Parallel.ForEach(
                        projectGraph.ProjectNodes,
                        s_parallelOptions,
                        node =>
                        {
                            foreach (ProjectCacheDescriptor projectCacheDescriptor in GetProjectCacheDescriptors(node.ProjectInstance))
                            {
                                // Intentionally fire-and-forget to asynchronously initialize the plugin. Any exceptions will bubble up later when querying.
                                _ = GetProjectCachePluginAsync(projectCacheDescriptor, projectGraph, buildRequestConfiguration: null, requestedTargets, cancellationToken)
                                    .ContinueWith(t => { }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted);
                            }
                        });
                },
                cancellationToken);
        }
 
        public void InitializePluginsForVsScenario(
            IEnumerable<ProjectCacheDescriptor> projectCacheDescriptors,
            BuildRequestConfiguration buildRequestConfiguration,
            ICollection<string> requestedTargets,
            CancellationToken cancellationToken)
        {
            EnsureNotDisposed();
 
            _isVsScenario = true;
 
            // Bail out for design-time builds
            if (IsDesignTimeBuild(buildRequestConfiguration))
            {
                return;
            }
 
            // Performing this in a Task.Run to break away from the main thread and prevent hangs
            Task.Run(
                () =>
                {
                    Parallel.ForEach(
                        projectCacheDescriptors,
                        s_parallelOptions,
                        projectCacheDescriptor =>
                        {
                            // Intentionally fire-and-forget to asynchronously initialize the plugin. Any exceptions will bubble up later when querying.
                            _ = GetProjectCachePluginAsync(projectCacheDescriptor, projectGraph: null, buildRequestConfiguration, requestedTargets, cancellationToken)
                                .ContinueWith(t => { }, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.OnlyOnFaulted);
                        });
                },
                cancellationToken);
        }
 
        private Task<ProjectCachePlugin> GetProjectCachePluginAsync(
            ProjectCacheDescriptor projectCacheDescriptor,
            ProjectGraph? projectGraph,
            BuildRequestConfiguration? buildRequestConfiguration,
            ICollection<string> requestedTargets,
            CancellationToken cancellationToken)
            => _projectCachePlugins.GetOrAdd(
                projectCacheDescriptor,
                // The use of Lazy is because ConcurrentDictionary doesn't guarantee the value factory executes only once if there are multiple simultaneous callers,
                // so this ensures that CreateAndInitializePluginAsync is only called exactly once.
                descriptor => new Lazy<Task<ProjectCachePlugin>>(() => CreateAndInitializePluginAsync(descriptor, projectGraph, buildRequestConfiguration, requestedTargets, cancellationToken)))
               .Value;
 
        private IEnumerable<ProjectCacheDescriptor> GetProjectCacheDescriptors(ProjectInstance projectInstance)
        {
            if (_globalProjectCacheDescriptor != null)
            {
                yield return _globalProjectCacheDescriptor;
            }
 
            ICollection<ProjectItemInstance> items = projectInstance.GetItems(ItemTypeNames.ProjectCachePlugin);
            foreach (ProjectItemInstance item in items)
            {
                string pluginPath = FileUtilities.NormalizePath(Path.Combine(item.Project.Directory, item.EvaluatedInclude));
 
                var pluginSettings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                foreach (ProjectMetadataInstance metadatum in item.Metadata)
                {
                    pluginSettings.Add(metadatum.Name, metadatum.EvaluatedValue);
                }
 
                yield return ProjectCacheDescriptor.FromAssemblyPath(pluginPath, pluginSettings);
            }
        }
 
        private async Task<ProjectCachePlugin> CreateAndInitializePluginAsync(
            ProjectCacheDescriptor projectCacheDescriptor,
            ProjectGraph? projectGraph,
            BuildRequestConfiguration? buildRequestConfiguration,
            ICollection<string> requestedTargets,
            CancellationToken cancellationToken)
        {
            BuildEventContext buildEventContext = BuildEventContext.Invalid;
            BuildEventFileInfo buildEventFileInfo = BuildEventFileInfo.Empty;
            var pluginLogger = new LoggingServiceToPluginLoggerAdapter(
                _loggingService,
                buildEventContext,
                buildEventFileInfo);
 
            ProjectCachePluginBase pluginInstance;
            string pluginTypeName;
 
            if (projectCacheDescriptor.PluginInstance != null)
            {
                pluginInstance = projectCacheDescriptor.PluginInstance;
                pluginTypeName = projectCacheDescriptor.PluginInstance.GetType().Name;
            }
            else
            {
                ErrorUtilities.VerifyThrowArgumentNull(projectCacheDescriptor.PluginAssemblyPath, nameof(projectCacheDescriptor.PluginAssemblyPath));
 
                string pluginAssemblyPath = projectCacheDescriptor.PluginAssemblyPath!;
                pluginTypeName = pluginAssemblyPath; // Just in case the assembly can't be loaded, the path would be helpful to help identify the problem.
                try
                {
                    MSBuildEventSource.Log.ProjectCacheCreatePluginInstanceStart(pluginAssemblyPath);
 
                    Type pluginType = GetTypeFromAssemblyPath(pluginAssemblyPath);
                    pluginTypeName = pluginType.Name;
 
                    pluginInstance = GetPluginInstanceFromType(pluginType);
                }
                catch (Exception e)
                {
                    return new ProjectCachePlugin(
                        pluginTypeName,
                        Instance: null,
#if FEATURE_REPORTFILEACCESSES
                        HandlerRegistration: null,
#endif
                        ExceptionDispatchInfo.Capture(e));
                }
                finally
                {
                    MSBuildEventSource.Log.ProjectCacheCreatePluginInstanceStop(pluginAssemblyPath, pluginTypeName);
                }
            }
 
            IReadOnlyCollection<ProjectGraphEntryPoint>? graphEntryPoints = buildRequestConfiguration != null
                ? GetGraphEntryPoints(buildRequestConfiguration)
                : null;
 
            // In practice, the underlying type of the ICollection is a List<string> so attempt to cast first
            IReadOnlyList<string> requestedTargetsList = requestedTargets as List<string> ?? requestedTargets.ToList();
 
            _loggingService.LogComment(buildEventContext, MessageImportance.High, "LoadingProjectCachePlugin", pluginTypeName);
            MSBuildEventSource.Log.ProjectCacheBeginBuildStart(pluginTypeName);
 
            try
            {
                await pluginInstance.BeginBuildAsync(
                    new CacheContext(
                        projectCacheDescriptor.PluginSettings,
                        DefaultMSBuildFileSystem.Instance,
                        requestedTargetsList,
                        projectGraph,
                        graphEntryPoints),
                    pluginLogger,
                    cancellationToken);
 
                if (pluginLogger.HasLoggedErrors)
                {
                    ProjectCacheException.ThrowForErrorLoggedInsideTheProjectCache("ProjectCacheInitializationFailed");
                }
 
#if FEATURE_REPORTFILEACCESSES
                FileAccessManager.HandlerRegistration? handlerRegistration = null;
                if (_componentHost.BuildParameters.ReportFileAccesses)
                {
                    handlerRegistration = _fileAccessManager.RegisterHandlers(
                        (buildRequest, fileAccessData) =>
                        {
                            // TODO: Filter out projects which do not configure this plugin
                            FileAccessContext fileAccessContext = GetFileAccessContext(buildRequest);
                            pluginInstance.HandleFileAccess(fileAccessContext, fileAccessData);
                        },
                        (buildRequest, processData) =>
                        {
                            // TODO: Filter out projects which do not configure this plugin
                            FileAccessContext fileAccessContext = GetFileAccessContext(buildRequest);
                            pluginInstance.HandleProcess(fileAccessContext, processData);
                        });
                }
#endif
 
                return new ProjectCachePlugin(
                    pluginTypeName,
                    pluginInstance,
#if FEATURE_REPORTFILEACCESSES
                    handlerRegistration,
#endif
                    InitializationException: null);
            }
            catch (Exception e)
            {
                return new ProjectCachePlugin(
                    pluginTypeName,
                    Instance: null,
#if FEATURE_REPORTFILEACCESSES
                    HandlerRegistration: null,
#endif
                    ExceptionDispatchInfo.Capture(e));
            }
            finally
            {
                MSBuildEventSource.Log.ProjectCacheBeginBuildStop(pluginTypeName);
            }
        }
 
        private FileAccessContext GetFileAccessContext(BuildRequest buildRequest)
        {
            BuildRequestConfiguration configuration = _configCache[buildRequest.ConfigurationId];
            IReadOnlyDictionary<string, string> globalProperties = GetGlobalProperties(configuration);
            return new FileAccessContext(configuration.ProjectFullPath, globalProperties, buildRequest.Targets);
        }
 
        private IReadOnlyDictionary<string, string> GetGlobalProperties(BuildRequestConfiguration configuration)
            => _globalPropertiesPerConfiguration.GetOrAdd(
                    configuration,
                    static configuration =>
                    {
                        Dictionary<string, string> globalProperties = new(configuration.GlobalProperties.Count, StringComparer.OrdinalIgnoreCase);
                        foreach (ProjectPropertyInstance property in configuration.GlobalProperties)
                        {
                            globalProperties.Add(property.Name, property.EvaluatedValue);
                        }
 
                        return globalProperties;
                    });
 
        private static ProjectCachePluginBase GetPluginInstanceFromType(Type pluginType)
        {
            try
            {
                return (ProjectCachePluginBase)Activator.CreateInstance(pluginType)!;
            }
            catch (TargetInvocationException e) when (e.InnerException != null)
            {
                HandlePluginException(e.InnerException, "Constructor");
                return null!; // Unreachable
            }
        }
 
        private static Type GetTypeFromAssemblyPath(string pluginAssemblyPath)
        {
            var assembly = LoadAssembly(pluginAssemblyPath);
 
            var type = GetTypes<ProjectCachePluginBase>(assembly).FirstOrDefault();
 
            if (type == null)
            {
                ProjectCacheException.ThrowForMSBuildIssueWithTheProjectCache("NoProjectCachePluginFoundInAssembly", pluginAssemblyPath);
            }
 
            return type!;
 
            Assembly LoadAssembly(string resolverPath)
            {
#if !FEATURE_ASSEMBLYLOADCONTEXT
                return Assembly.LoadFrom(resolverPath);
#else
                return s_loader.LoadFromPath(resolverPath);
#endif
            }
 
            IEnumerable<Type> GetTypes<T>(Assembly assembly)
            {
                return assembly.ExportedTypes
                    .Select(type => new { type, info = type.GetTypeInfo() })
                    .Where(
                        t => t.info.IsClass &&
                             t.info.IsPublic &&
                             !t.info.IsAbstract &&
                             typeof(T).IsAssignableFrom(t.type))
                    .Select(t => t.type);
            }
        }
 
#if FEATURE_ASSEMBLYLOADCONTEXT
        private static readonly CoreClrAssemblyLoader s_loader = new CoreClrAssemblyLoader();
#endif
 
        public bool ShouldUseCache(BuildRequestConfiguration buildRequestConfiguration)
        {
            if (IsDesignTimeBuild(buildRequestConfiguration))
            {
                return false;
            }
 
            if (_globalProjectCacheDescriptor != null)
            {
                return true;
            }
 
            // We've determined it's the VS scenario and know that there are project cache plugins.
            if (_isVsScenario)
            {
                return true;
            }
 
            // If the project isn't loaded, don't force it to be just to check if it's cacheable as this may not be very performant.
            if (!buildRequestConfiguration.IsLoaded)
            {
                return false;
            }
 
            // We need to retrieve the configuration if it's already loaded in order to access the Project property below.
            if (buildRequestConfiguration.IsCached)
            {
                buildRequestConfiguration.RetrieveFromCache();
            }
 
            // Check if there are any project cache items defined in the project
            return GetProjectCacheDescriptors(buildRequestConfiguration.Project).Any();
        }
 
        private bool IsDesignTimeBuild(BuildRequestConfiguration buildRequestConfiguration)
        {
            string? designTimeBuild = buildRequestConfiguration.GlobalProperties[DesignTimeProperties.DesignTimeBuild]?.EvaluatedValue;
            string? buildingProject = buildRequestConfiguration.GlobalProperties[DesignTimeProperties.BuildingProject]?.EvaluatedValue;
            return ConversionUtilities.ConvertStringToBool(designTimeBuild, nullOrWhitespaceIsFalse: true)
                || (buildingProject != null && !ConversionUtilities.ConvertStringToBool(buildingProject, nullOrWhitespaceIsFalse: true));
        }
 
        public void PostCacheRequest(CacheRequest cacheRequest, CancellationToken cancellationToken)
        {
            EnsureNotDisposed();
 
            Task.Run(
                async () =>
                {
                    try
                    {
                        (CacheResult cacheResult, int projectContextId) = await ProcessCacheRequestAsync();
                        _buildManager.PostCacheResult(cacheRequest, cacheResult, projectContextId);
                    }
                    catch (Exception e)
                    {
                        _buildManager.PostCacheResult(cacheRequest, CacheResult.IndicateException(e), BuildEventContext.InvalidProjectContextId);
                    }
                },
                cancellationToken);
 
            async Task<(CacheResult Result, int ProjectContextId)> ProcessCacheRequestAsync()
            {
                EvaluateProjectIfNecessary(cacheRequest.Submission, cacheRequest.Configuration);
 
                BuildRequestData buildRequest = new BuildRequestData(
                    cacheRequest.Configuration.Project,
                    cacheRequest.Submission.BuildRequestData?.TargetNames.ToArray() ?? []);
                BuildEventContext buildEventContext = _loggingService.CreateProjectCacheBuildEventContext(
                    cacheRequest.Submission.SubmissionId,
                    evaluationId: cacheRequest.Configuration.Project.EvaluationId,
                    projectInstanceId: cacheRequest.Configuration.ConfigurationId,
                    projectFile: cacheRequest.Configuration.Project.FullPath);
 
                CacheResult cacheResult;
                try
                {
                    cacheResult = await GetCacheResultAsync(buildRequest, cacheRequest.Configuration, buildEventContext, cancellationToken);
                }
                catch (Exception ex)
                {
                    // Wrap the exception here so we can preserve the ProjectContextId
                    cacheResult = CacheResult.IndicateException(ex);
                }
 
                return (cacheResult, buildEventContext.ProjectContextId);
            }
 
            void EvaluateProjectIfNecessary(BuildSubmission submission, BuildRequestConfiguration configuration)
            {
                ErrorUtilities.VerifyThrow(submission.BuildRequestData != null,
                    "Submission BuildRequestData is not populated.");
 
                lock (configuration)
                {
                    if (!configuration.IsLoaded)
                    {
                        configuration.LoadProjectIntoConfiguration(
                            _buildManager,
                            submission.BuildRequestData!.Flags,
                            submission.SubmissionId,
                            Scheduler.InProcNodeId);
 
                        // If we're taking the time to evaluate, avoid having other nodes to repeat the same evaluation.
                        // Based on the assumption that ProjectInstance serialization is faster than evaluating from scratch.
                        configuration.Project.TranslateEntireState = true;
                    }
                }
            }
        }
 
        private async Task<CacheResult> GetCacheResultAsync(BuildRequestData buildRequest, BuildRequestConfiguration buildRequestConfiguration, BuildEventContext buildEventContext, CancellationToken cancellationToken)
        {
            ErrorUtilities.VerifyThrowInternalNull(buildRequest.ProjectInstance, nameof(buildRequest.ProjectInstance));
 
            var buildEventFileInfo = new BuildEventFileInfo(buildRequest.ProjectFullPath);
            var pluginLogger = new LoggingServiceToPluginLoggerAdapter(
                _loggingService,
                buildEventContext,
                buildEventFileInfo);
 
            string? targetNames = buildRequest.TargetNames != null && buildRequest.TargetNames.Count > 0
                ? string.Join(", ", buildRequest.TargetNames)
                : null;
            if (string.IsNullOrEmpty(targetNames))
            {
                _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheQueryStartedWithDefaultTargets", buildRequest.ProjectFullPath);
            }
            else
            {
                _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheQueryStartedWithTargetNames", buildRequest.ProjectFullPath, targetNames);
            }
 
            HashSet<ProjectCacheDescriptor> queriedCaches = new(ProjectCacheDescriptorEqualityComparer.Instance);
            CacheResult? cacheResult = null;
            foreach (ProjectCacheDescriptor projectCacheDescriptor in GetProjectCacheDescriptors(buildRequest.ProjectInstance!))
            {
                // Ensure each unique plugin is only queried once
                if (!queriedCaches.Add(projectCacheDescriptor))
                {
                    continue;
                }
 
                ICollection<string> requestedTargetsList = buildRequestConfiguration.RequestedTargets as ICollection<string> ?? buildRequestConfiguration.RequestedTargets.ToList();
                ProjectCachePlugin plugin = await GetProjectCachePluginAsync(projectCacheDescriptor, projectGraph: null, buildRequestConfiguration, requestedTargetsList, cancellationToken);
                try
                {
                    // Rethrow any initialization exception.
                    plugin.InitializationException?.Throw();
 
                    ErrorUtilities.VerifyThrow(plugin.Instance != null, "Plugin '{0}' instance is null", plugin.Name);
 
                    MSBuildEventSource.Log.ProjectCacheGetCacheResultStart(plugin.Name, buildRequest.ProjectFullPath, targetNames ?? MSBuildConstants.DefaultTargetsMarker);
                    cacheResult = await plugin.Instance!.GetCacheResultAsync(buildRequest, pluginLogger, cancellationToken);
 
                    if (pluginLogger.HasLoggedErrors || cacheResult.ResultType == CacheResultType.None)
                    {
                        ProjectCacheException.ThrowForErrorLoggedInsideTheProjectCache("ProjectCacheQueryFailed", buildRequest.ProjectFullPath);
                    }
 
                    if (cacheResult.ResultType == CacheResultType.CacheHit)
                    {
                        break;
                    }
                }
                catch (Exception e) when (e is not ProjectCacheException)
                {
                    HandlePluginException(e, nameof(ProjectCachePluginBase.GetCacheResultAsync));
                    return null!; // Unreachable
                }
                finally
                {
                    if (MSBuildEventSource.Log.IsEnabled())
                    {
                        string cacheResultType = cacheResult?.ResultType.ToString() ?? nameof(CacheResultType.None);
                        MSBuildEventSource.Log.ProjectCacheGetCacheResultStop(plugin.Name, buildRequest.ProjectFullPath, targetNames ?? "<default>", cacheResultType);
                    }
                }
            }
 
            // Handle the case of no configured plugins.
            cacheResult ??= CacheResult.IndicateNonCacheHit(CacheResultType.CacheNotApplicable);
 
            switch (cacheResult.ResultType)
            {
                case CacheResultType.CacheHit:
                    if (string.IsNullOrEmpty(targetNames))
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheHitWithDefaultTargets", buildRequest.ProjectFullPath);
                    }
                    else
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheHitWithTargetNames", buildRequest.ProjectFullPath, targetNames);
                    }
 
                    // Similar to CopyFilesToOutputDirectory from Microsoft.Common.CurrentVersion.targets, so that progress can be seen.
                    // TODO: This should be indented by the console logger. That requires making these log events structured.
                    if (!buildRequestConfiguration.IsTraversal)
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.High, "ProjectCacheHitWithOutputs", buildRequest.ProjectInstance!.GetPropertyValue(ReservedPropertyNames.projectName));
                    }
 
                    break;
                case CacheResultType.CacheMiss:
                    if (string.IsNullOrEmpty(targetNames))
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheMissWithDefaultTargets", buildRequest.ProjectFullPath);
                    }
                    else
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheMissWithTargetNames", buildRequest.ProjectFullPath, targetNames);
                    }
 
                    break;
                case CacheResultType.CacheNotApplicable:
                    if (string.IsNullOrEmpty(targetNames))
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheNotApplicableWithDefaultTargets", buildRequest.ProjectFullPath);
                    }
                    else
                    {
                        _loggingService.LogComment(buildEventContext, MessageImportance.Normal, "ProjectCacheNotApplicableWithTargetNames", buildRequest.ProjectFullPath, targetNames);
                    }
 
                    break;
                case CacheResultType.None: // Should not get here based on the throw above
                default:
                    throw new ArgumentOutOfRangeException();
            }
 
            return cacheResult;
        }
 
        private IReadOnlyCollection<ProjectGraphEntryPoint> GetGraphEntryPoints(BuildRequestConfiguration configuration)
        {
            var globalProperties = new Dictionary<string, string>(configuration.GlobalProperties.Count, StringComparer.OrdinalIgnoreCase);
            foreach (ProjectPropertyInstance property in configuration.GlobalProperties)
            {
                // If any project specific property is set, it will propagate down the project graph and force all nodes to that property's specific side effects, which is incorrect.
                if (!s_projectSpecificPropertyNames.Contains(property.Name))
                {
                    globalProperties.Add(property.Name, property.EvaluatedValue);
                }
            }
 
            if (globalProperties.TryGetValue(SolutionProjectGenerator.CurrentSolutionConfigurationContents, out string? solutionConfigurationXml)
                && !string.IsNullOrWhiteSpace(solutionConfigurationXml))
            {
                // A solution supports multiple solution configurations (different values for Configuration and Platform).
                // Each solution configuration generates a different static graph.
                // Therefore, plugin implementations that rely on creating static graphs in their BeginBuild methods need access to the
                // currently solution configuration used by VS.
                //
                // In this VS workaround, however, we do not have access to VS' solution configuration. The only information we have is a global property
                // named "CurrentSolutionConfigurationContents" that VS sets on each built project. It does not contain the solution configuration itself, but
                // instead it contains information on how the solution configuration maps to each project's configuration.
                //
                // So instead of using the solution file as the entry point, we parse this VS property and extract graph entry points from it, for every project
                // mentioned in the "CurrentSolutionConfigurationContents" global property.
                return GenerateGraphEntryPointsFromSolutionConfigurationXml(solutionConfigurationXml!, configuration.ProjectFullPath, globalProperties);
            }
            else
            {
                return [new ProjectGraphEntryPoint(configuration.ProjectFullPath, globalProperties)];
            }
 
            static IReadOnlyCollection<ProjectGraphEntryPoint> GenerateGraphEntryPointsFromSolutionConfigurationXml(
                string solutionConfigurationXml,
                string definingProjectPath,
                Dictionary<string, string> templateGlobalProperties)
            {
                XmlNodeList? projectConfigurations = SolutionConfiguration.GetProjectConfigurations(solutionConfigurationXml);
                if (projectConfigurations == null || projectConfigurations.Count == 0)
                {
                    return Array.Empty<ProjectGraphEntryPoint>();
                }
 
                var graphEntryPoints = new List<ProjectGraphEntryPoint>(projectConfigurations.Count);
 
                foreach (XmlElement projectConfiguration in projectConfigurations)
                {
                    ErrorUtilities.VerifyThrowInternalNull(projectConfiguration.Attributes, nameof(projectConfiguration.Attributes));
 
                    var buildProjectInSolution = projectConfiguration.Attributes![SolutionConfiguration.BuildProjectInSolutionAttribute];
                    if (buildProjectInSolution is not null &&
                        string.IsNullOrWhiteSpace(buildProjectInSolution.Value) is false &&
                        bool.TryParse(buildProjectInSolution.Value, out var buildProject) &&
                        buildProject is false)
                    {
                        continue;
                    }
 
                    XmlAttribute? projectPathAttribute = projectConfiguration.Attributes![SolutionConfiguration.AbsolutePathAttribute];
                    ErrorUtilities.VerifyThrow(projectPathAttribute is not null, "Expected VS to set the project path on each ProjectConfiguration element.");
 
                    string projectPath = projectPathAttribute!.Value;
 
                    (string configuration, string platform) = SolutionFile.ParseConfigurationName(projectConfiguration.InnerText, definingProjectPath, 0, solutionConfigurationXml);
 
                    // Take the defining project global properties and override the configuration and platform.
                    // It's sufficient to only set Configuration and Platform.
                    // But we send everything to maximize the plugins' potential to quickly workaround potential MSBuild issues while the MSBuild fixes flow into VS.
                    var globalProperties = new Dictionary<string, string>(templateGlobalProperties, StringComparer.OrdinalIgnoreCase)
                    {
                        ["Configuration"] = configuration,
                        ["Platform"] = platform
                    };
 
                    graphEntryPoints.Add(new ProjectGraphEntryPoint(projectPath, globalProperties));
                }
 
                return graphEntryPoints;
            }
        }
 
        public async Task HandleBuildResultAsync(
            BuildRequestConfiguration requestConfiguration,
            BuildResult buildResult,
            BuildEventContext buildEventContext,
            CancellationToken cancellationToken)
        {
            ErrorUtilities.VerifyThrowInternalNull(requestConfiguration.Project, nameof(requestConfiguration.Project));
 
            if (_projectCachePlugins.IsEmpty)
            {
                return;
            }
 
            // We need to retrieve the configuration if it's already loaded in order to access the Project property below.
            if (requestConfiguration.IsCached)
            {
                requestConfiguration.RetrieveFromCache();
            }
 
            // Filter to plugins which apply to the project, if any
            List<ProjectCacheDescriptor> projectCacheDescriptors = GetProjectCacheDescriptors(requestConfiguration.Project).ToList();
            if (projectCacheDescriptors.Count == 0)
            {
                return;
            }
 
#if FEATURE_REPORTFILEACCESSES
            if (_componentHost.BuildParameters.ReportFileAccesses)
            {
                _fileAccessManager.WaitForFileAccessReportCompletion(buildResult.GlobalRequestId, cancellationToken);
            }
#endif
 
            IReadOnlyDictionary<string, string> globalProperties = GetGlobalProperties(requestConfiguration);
 
            List<string> targets = buildResult.ResultsByTarget?.Keys.ToList() ?? new();
            string? targetNames = string.Join(", ", targets);
 
            FileAccessContext fileAccessContext = new(requestConfiguration.ProjectFullPath, globalProperties, targets);
 
            var buildEventFileInfo = new BuildEventFileInfo(requestConfiguration.ProjectFullPath);
            var pluginLogger = new LoggingServiceToPluginLoggerAdapter(
                _loggingService,
                buildEventContext,
                buildEventFileInfo);
 
            Task[] tasks = new Task[projectCacheDescriptors.Count];
            int idx = 0;
            foreach (ProjectCacheDescriptor projectCacheDescriptor in projectCacheDescriptors)
            {
                tasks[idx++] = Task.Run(
                    async () =>
                    {
                        if (!_projectCachePlugins.TryGetValue(projectCacheDescriptor, out Lazy<Task<ProjectCachePlugin>>? pluginLazyTask))
                        {
                            // The plugin might not be in the collection if it was never initialized, which can happen if there are multiple plugins
                            // and the first one(s) always handles the cache request so the subsequent one(s) never get lazy initialized.
                            return;
                        }
 
                        ProjectCachePlugin plugin = await pluginLazyTask.Value;
 
                        // Rethrow any initialization exception.
                        plugin.InitializationException?.Throw();
 
                        ErrorUtilities.VerifyThrow(plugin.Instance != null, "Plugin '{0}' instance is null", plugin.Name);
 
                        MSBuildEventSource.Log.ProjectCacheHandleBuildResultStart(plugin.Name, fileAccessContext.ProjectFullPath, targetNames);
                        try
                        {
                            await plugin.Instance!.HandleProjectFinishedAsync(fileAccessContext, buildResult, pluginLogger, cancellationToken);
                        }
                        catch (Exception e) when (e is not ProjectCacheException)
                        {
                            HandlePluginException(e, nameof(ProjectCachePluginBase.HandleProjectFinishedAsync));
                        }
                        finally
                        {
                            MSBuildEventSource.Log.ProjectCacheHandleBuildResultStop(plugin.Name, fileAccessContext.ProjectFullPath, targetNames);
                        }
                    },
                    cancellationToken);
            }
 
            await Task.WhenAll(tasks).ConfigureAwait(false);
 
            if (pluginLogger.HasLoggedErrors)
            {
                ProjectCacheException.ThrowForErrorLoggedInsideTheProjectCache("ProjectCacheHandleBuildResultFailed", fileAccessContext.ProjectFullPath);
            }
        }
 
        public async ValueTask DisposeAsync()
        {
            if (_isDisposed)
            {
                return;
            }
 
            _isDisposed = true;
 
            if (_projectCachePlugins.IsEmpty)
            {
                return;
            }
 
            BuildEventContext buildEventContext = BuildEventContext.Invalid;
            BuildEventFileInfo buildEventFileInfo = BuildEventFileInfo.Empty;
            var pluginLogger = new LoggingServiceToPluginLoggerAdapter(
                _loggingService,
                buildEventContext,
                buildEventFileInfo);
 
            _loggingService.LogComment(buildEventContext, MessageImportance.Low, "ProjectCacheEndBuild");
 
            Task[] cleanupTasks = new Task[_projectCachePlugins.Count];
            int idx = 0;
            foreach (KeyValuePair<ProjectCacheDescriptor, Lazy<Task<ProjectCachePlugin>>> kvp in _projectCachePlugins)
            {
                cleanupTasks[idx++] = Task.Run(async () =>
                {
                    ProjectCachePlugin plugin = await kvp.Value.Value;
 
                    // If there is no instance, the exceptions would have bubbled up already, so skip cleanup for this one.
                    if (plugin.Instance == null)
                    {
                        return;
                    }
 
#if FEATURE_REPORTFILEACCESSES
                    if (plugin.HandlerRegistration.HasValue)
                    {
                        plugin.HandlerRegistration.Value.Dispose();
                    }
#endif
 
                    MSBuildEventSource.Log.ProjectCacheEndBuildStart(plugin.Name);
                    try
                    {
                        await plugin.Instance.EndBuildAsync(pluginLogger, CancellationToken.None);
                    }
                    catch (Exception e) when (e is not ProjectCacheException)
                    {
                        HandlePluginException(e, nameof(ProjectCachePluginBase.EndBuildAsync));
                    }
                    finally
                    {
                        MSBuildEventSource.Log.ProjectCacheEndBuildStop(plugin.Name);
                    }
                });
            }
 
            await Task.WhenAll(cleanupTasks).ConfigureAwait(false);
 
            if (pluginLogger.HasLoggedErrors)
            {
                ProjectCacheException.ThrowForErrorLoggedInsideTheProjectCache("ProjectCacheShutdownFailed");
            }
        }
 
        private void EnsureNotDisposed()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(nameof(ProjectCacheService));
            }
        }
 
        private static void HandlePluginException(Exception e, string apiExceptionWasThrownFrom)
        {
            if (ExceptionHandling.IsCriticalException(e))
            {
                throw e;
            }
 
            ProjectCacheException.ThrowAsUnhandledException(
                e,
                "ProjectCacheException",
                apiExceptionWasThrownFrom);
        }
 
        private class LoggingServiceToPluginLoggerAdapter : PluginLoggerBase
        {
            private readonly ILoggingService _loggingService;
 
            private readonly BuildEventContext _buildEventContext;
 
            private readonly BuildEventFileInfo _buildEventFileInfo;
 
            public override bool HasLoggedErrors { get; protected set; }
 
            public LoggingServiceToPluginLoggerAdapter(
                ILoggingService loggingService,
                BuildEventContext buildEventContext,
                BuildEventFileInfo buildEventFileInfo)
            {
                _loggingService = loggingService;
                _buildEventContext = buildEventContext;
                _buildEventFileInfo = buildEventFileInfo;
            }
 
            public override void LogMessage(string message, MessageImportance? messageImportance = null)
            {
                _loggingService.LogCommentFromText(
                    _buildEventContext,
                    messageImportance ?? MessageImportance.Normal,
                    message);
            }
 
            public override void LogWarning(string warning)
            {
                _loggingService.LogWarningFromText(
                    _buildEventContext,
                    subcategoryResourceName: null,
                    warningCode: null,
                    helpKeyword: null,
                    _buildEventFileInfo,
                    warning);
            }
 
            public override void LogError(string error)
            {
                HasLoggedErrors = true;
 
                _loggingService.LogErrorFromText(
                    _buildEventContext,
                    subcategoryResourceName: null,
                    errorCode: null,
                    helpKeyword: null,
                    _buildEventFileInfo,
                    error);
            }
        }
    }
}