File: BuildService.cs
Web Access
Project: Microsoft.DotNet.ProjectTools.csproj (Microsoft.DotNet.ProjectTools)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Xml;

namespace Microsoft.DotNet.FileBasedPrograms;

public sealed class BuildService : IBuildService
{
    public static BuildService Instance { get; } = new();

    private BuildService() { }

    public ValueTask<IProjectInstance> CreateProjectInstanceFromProjectRootElementAsync(
        IProjectRootElement projectRoot,
        IProjectCollection projectCollection,
        IDictionary<string, string>? additionalGlobalProperties)
    {
        return new(ProjectInstance.FromProjectRootElement((ProjectRootElement)projectRoot, (ProjectCollection)projectCollection, additionalGlobalProperties));
    }

    public IProjectRootElement CreateProjectRootElement(XmlReader xmlReader, IProjectCollection projectCollection, string entryPointFilePath)
    {
        return ProjectRootElement.Create(xmlReader, (ProjectCollection)projectCollection, entryPointFilePath);
    }
}

public static class BuildServiceExtensions
{
    private static readonly ConditionalWeakTable<Microsoft.Build.Evaluation.ProjectCollection, IProjectCollection> s_projectCollections = new();

    extension(Microsoft.Build.Evaluation.ProjectCollection projectCollection)
    {
        public IProjectCollection Wrap()
        {
            return s_projectCollections.GetValue(projectCollection, static inner => new ProjectCollection(inner));
        }
    }

    extension(Microsoft.Build.Execution.ProjectInstance projectInstance)
    {
        public IProjectInstance Wrap()
        {
            return new ProjectInstance(projectInstance);
        }
    }

    extension(IProjectRootElement projectRootElement)
    {
        public Microsoft.Build.Construction.ProjectRootElement Unwrap()
        {
            return ((ProjectRootElement)projectRootElement).Inner;
        }
    }

    extension(IProjectInstance projectInstance)
    {
        public Microsoft.Build.Execution.ProjectInstance Unwrap()
        {
            return ((ProjectInstance)projectInstance).Inner;
        }
    }
}

sealed file class ProjectCollection(Microsoft.Build.Evaluation.ProjectCollection inner) : IProjectCollection
{
    /// <summary>
    /// Keeps a strong reference to the latest virtual <see cref="IProjectRootElement"/> registered for each entry point in this <see cref="IProjectCollection"/>,
    /// preventing it from being garbage collected when MSBuild's <c>ProjectRootElementCache</c> demotes it to a weak reference.
    /// Without this, nested <c>&lt;MSBuild&gt;</c> tasks that re-evaluate virtual projects with different properties
    /// would fail to find the <see cref="IProjectRootElement"/> in the cache and try to load it from disk,
    /// resulting in MSB4025 because the virtual project file does not exist on disk.
    /// See <see href="https://github.com/dotnet/sdk/issues/52714"/>.
    /// Access is synchronized by locking this dictionary directly.
    /// </summary>
    private readonly Dictionary<string, Microsoft.Build.Construction.ProjectRootElement> _projectRootElements = new(StringComparer.OrdinalIgnoreCase);

    public Microsoft.Build.Evaluation.ProjectCollection Inner => inner;

    public void SetProjectRootElementFullPath(
        string entryPointFilePath,
        Microsoft.Build.Construction.ProjectRootElement projectRootElement,
        string? fullPath)
    {
        lock (_projectRootElements)
        {
            var previousFullPath = projectRootElement.FullPath;

            // FullPath registers the root in MSBuild's cache, so registration and retention must share this lock.
            projectRootElement.FullPath = fullPath;

            // Compare after assignment because FullPath normalizes the input before checking for a no-op.
            // An unchanged normalized path does not replace MSBuild's cached root.
            if (!string.Equals(previousFullPath, projectRootElement.FullPath, StringComparison.OrdinalIgnoreCase))
            {
                _projectRootElements[entryPointFilePath] = projectRootElement;
            }
        }
    }
}

sealed file class ProjectInstance(Microsoft.Build.Execution.ProjectInstance inner) : IProjectInstance
{
    public static ProjectInstance FromProjectRootElement(
        ProjectRootElement projectRoot,
        ProjectCollection projectCollection,
        IDictionary<string, string>? additionalGlobalProperties)
    {
        var mergedGlobalProperties = new Dictionary<string, string>(projectCollection.Inner.GlobalProperties, StringComparer.OrdinalIgnoreCase);

        if (additionalGlobalProperties != null)
        {
            foreach (var pair in additionalGlobalProperties)
            {
                mergedGlobalProperties[pair.Key] = pair.Value;
            }
        }

        return new(Microsoft.Build.Execution.ProjectInstance.FromProjectRootElement(projectRoot.Inner, new()
        {
            ProjectCollection = projectCollection.Inner,
            GlobalProperties = mergedGlobalProperties,
        }));
    }

    public Microsoft.Build.Execution.ProjectInstance Inner => inner;
    public ValueTask<ImmutableArray<ImmutableArray<string>>> GetItemMetadataValuesAsync(string itemType, ImmutableArray<string> metadataNames)
        => new(inner.GetItems(itemType).Select(i => metadataNames.Select(n => i.GetMetadataValue(n)).ToImmutableArray()).ToImmutableArray());
    public ValueTask<string> GetPropertyValueAsync(string propertyName) => new(inner.GetPropertyValue(propertyName));
    public ValueTask<string> ExpandStringAsync(string value) => new(inner.ExpandString(value));
}

sealed file class ProjectRootElement(
    Microsoft.Build.Construction.ProjectRootElement inner,
    ProjectCollection projectCollection,
    string entryPointFilePath) : IProjectRootElement
{
    public static ProjectRootElement Create(XmlReader xmlReader, ProjectCollection projectCollection, string entryPointFilePath)
    {
        var inner = Microsoft.Build.Construction.ProjectRootElement.Create(xmlReader, projectCollection.Inner);
        return new(inner, projectCollection, entryPointFilePath);
    }

    public Microsoft.Build.Construction.ProjectRootElement Inner => inner;
    public string? FullPath
    {
        get => inner.FullPath;
        set => projectCollection.SetProjectRootElementFullPath(entryPointFilePath, inner, value);
    }
    public string GetRawXml() => inner.RawXml;
}