File: Build\ProjectGraphFactory.cs
Web Access
Project: src\src\sdk\src\Dotnet.Watch\Watch\Microsoft.DotNet.HotReload.Watch.csproj (Microsoft.DotNet.HotReload.Watch)
// 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.Diagnostics;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
using Microsoft.DotNet.ProjectTools;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Microsoft.DotNet.Watch;

internal sealed class ProjectGraphFactory(
    ImmutableArray<ProjectRepresentation> rootProjects,
    ImmutableDictionary<string, string> buildProperties,
    ILogger logger,
    GlobalOptions globalOptions,
    EnvironmentOptions environmentOptions)
{
    /// <summary>
    /// Reuse <see cref="ProjectCollection"/> with XML element caching to improve performance.
    ///
    /// The cache is automatically updated when build files change.
    /// https://github.com/dotnet/msbuild/blob/b6f853defccd64ae1e9c7cf140e7e4de68bff07c/src/Build/Definition/ProjectCollection.cs#L343-L354
    /// </summary>
    private readonly ProjectCollection _collection = new(
        globalProperties: buildProperties,
        loggers: [],
        remoteLoggers: [],
        ToolsetDefinitionLocations.Default,
        maxNodeCount: 1,
        onlyLogCriticalEvents: false,
        loadProjectsReadOnly: false,
        useAsynchronousLogging: false,
        reuseProjectRootElementCache: true);

    public ILogger Logger => logger;

    private static string GetProductTargetFramework()
    {
        var attribute = typeof(VirtualProjectBuilder).Assembly.GetCustomAttribute<TargetFrameworkAttribute>() ?? throw new InvalidOperationException();
        var version = new FrameworkName(attribute.FrameworkName).Version;
        return $"net{version.Major}.{version.Minor}";
    }

    /// <summary>
    /// Tries to create a project graph by running the build evaluation phase on root projects.
    /// </summary>
    public LoadedProjectGraph? TryLoadProjectGraph(bool projectGraphRequired, string? virtualProjectTargetFramework, CancellationToken cancellationToken)
    {
        virtualProjectTargetFramework ??= GetProductTargetFramework();

        var entryPoints = rootProjects.Select(p => new ProjectGraphEntryPoint(p.ProjectGraphPath, buildProperties));
        try
        {
            var stopwatch = Stopwatch.StartNew();
            var graph = new LoadedProjectGraph(
                new ProjectGraph(entryPoints, _collection, (path, globalProperties, collection) => CreateProjectInstance(path, globalProperties, collection, virtualProjectTargetFramework, logger), cancellationToken),
                _collection,
                logger,
                globalOptions,
                environmentOptions);

            logger.LogDebug("Project graph loaded in {Time}s.", stopwatch.Elapsed.TotalSeconds.ToString("0.0"));
            return graph;
        }
        catch (ProjectCreationFailedException)
        {
            // Errors have already been reported.
        }
        catch (Exception e) when (e is not OperationCanceledException)
        {
            // ProjectGraph aggregates OperationCanceledException exception,
            // throw here to propagate the cancellation.
            cancellationToken.ThrowIfCancellationRequested();

            logger.LogDebug("Failed to load project graph.");

            if (e is AggregateException { InnerExceptions: var innerExceptions })
            {
                foreach (var inner in innerExceptions)
                {
                    if (inner is not ProjectCreationFailedException)
                    {
                        Report(inner);
                    }
                }
            }
            else
            {
                Report(e);
            }

            void Report(Exception e)
            {
                if (projectGraphRequired)
                {
                    logger.LogError(e.Message);
                }
                else
                {
                    logger.LogWarning(e.Message);
                }
            }
        }

        return null;
    }

    private ProjectInstance CreateProjectInstance(string projectPath, Dictionary<string, string> globalProperties, ProjectCollection projectCollection, string virtualProjectTargetFramework, ILogger logger)
    {
        if (!File.Exists(projectPath))
        {
            // The virtual project path is the entry-point file path with ".csproj" appended (e.g., "App.cs.csproj" for "App.cs").
            if (!VirtualProjectBuilder.TryGetEntryPointFilePathFromVirtualProjectPath(projectPath, out string? entryPointFilePath))
            {
                // `dotnet build` reports a warning when the reference project is missing.
                // However, ProjectGraph doesn't allow us to return null to skip the project so we need to be stricter.
                logger.LogError("The project file could not be loaded. Could not find a part of the path '{Path}'.", projectPath);
                throw new ProjectCreationFailedException();
            }

            var anyError = false;

            var projectInstance = VirtualProjectBuilder.CreateProjectInstance(
                entryPointFilePath,
                virtualProjectTargetFramework,
                projectCollection,
                (path, line, message) =>
                {
                    anyError = true;
                    logger.LogError("{Path}({Line}): {Message}", path, line, message);
                });

            if (anyError)
            {
                throw new ProjectCreationFailedException();
            }

            return projectInstance;
        }

        return new ProjectInstance(
            projectPath,
            globalProperties,
            toolsVersion: "Current",
            subToolsetVersion: null,
            projectCollection);
    }

    private sealed class ProjectCreationFailedException() : Exception();
}