File: DotNet\DotNetRestoreConfiguration.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text.Json;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
 
namespace Aspire.Cli.DotNet;
 
/// <summary>
/// Inspects the current C# AppHost's evaluated MSBuild restore policy without executing targets.
/// </summary>
internal static class DotNetRestoreConfiguration
{
    // RestoreAdditionalProjectSources is additive and still uses the native configuration
    // hierarchy. Native restore retains those sources without overriding the candidate config.
    private static readonly string[] s_configurationProperties =
    [
        "RestoreConfigFile",
        "RestoreRootConfigDirectory",
        "RestoreSources",
        "_RestoreSourcesOverride"
    ];
 
    internal static async Task<DotNetRestoreSettings> ReadAsync(
        IDotNetCliRunner runner,
        FileInfo projectFile,
        CancellationToken cancellationToken)
    {
        return await TryReadAsync(runner, projectFile, cancellationToken) ??
            throw new ProjectUpdaterException(
                string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.FailedFetchItemsAndPropertiesFormat, projectFile.FullName));
    }
 
    internal static async Task<DotNetRestoreSettings?> TryReadAsync(
        IDotNetCliRunner runner,
        FileInfo projectFile,
        CancellationToken cancellationToken)
    {
        var (exitCode, output) = await runner.GetProjectItemsAndPropertiesAsync(
            projectFile,
            items: [],
            properties:
            [
                .. s_configurationProperties,
                "MSBuildProjectFullPath",
                "MSBuildToolsPath",
                "NuGetRestoreTargets"
            ],
            // Restore targets collect package references and can run legacy SDK validation
            // before the updater has repaired the project. Inspect policy through evaluation only.
            targets: [],
            new ProcessInvocationOptions
            {
                // File-based build otherwise performs implicit restore before property evaluation.
                // A global property prevents project imports from overriding NuGet's
                // restore-graph evaluation mode and loading stale generated package imports.
                NoRestore = true,
                ExcludeRestorePackageImports = true,
                SuppressLogging = true
            },
            cancellationToken);
        if (exitCode != 0 || output is null)
        {
            output?.Dispose();
            return null;
        }
 
        using (output)
        {
            // MSBuild emits {"Properties":{"MSBuildProjectFullPath":".../apphost.cs.csproj"}}.
            // Keep the synthetic identity for file-based apps; later MSBuild restore targets use it.
            var properties = output.RootElement.GetProperty("Properties");
            var overrides = s_configurationProperties
                .Where(name => !string.IsNullOrEmpty(GetProperty(properties, name)))
                .ToList();
            var restoreTargets = GetProperty(properties, "NuGetRestoreTargets");
            var defaultTargets = Path.Combine(GetProperty(properties, "MSBuildToolsPath"), "NuGet.targets");
            if (!string.IsNullOrEmpty(restoreTargets) &&
                !string.Equals(Path.GetFullPath(restoreTargets, projectFile.DirectoryName!),
                    Path.GetFullPath(defaultTargets), StringComparisons.FileSystemPath))
            {
                overrides.Add("NuGetRestoreTargets");
            }
 
            return new DotNetRestoreSettings(
                GetProperty(properties, "MSBuildProjectFullPath"),
                string.IsNullOrEmpty(restoreTargets) ? defaultTargets : restoreTargets,
                overrides);
        }
    }
 
    private static string GetProperty(JsonElement properties, string name)
        => properties.GetProperty(name).GetString() ?? string.Empty;
}
 
/// <summary>
/// Holds the current project's evaluated identity, restore targets, and configuration override names.
/// </summary>
internal sealed record DotNetRestoreSettings(
    string ProjectIdentity,
    string RestoreTargets,
    IReadOnlyList<string> ConfigurationOverrides)
{
    public bool UsesAmbientConfiguration => ConfigurationOverrides.Count == 0;
}