File: Commands\Reference\Add\ReferenceAddCommand.cs
Web Access
Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.CommandLine;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Commands.Package;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectTools;
using NuGet.Frameworks;

namespace Microsoft.DotNet.Cli.Commands.Reference.Add;

internal sealed class ReferenceAddCommand : CommandBase<ReferenceAddCommandDefinitionBase>
{
    private readonly string _fileOrDirectory;
    private readonly AppKinds _allowedAppKinds;

    public ReferenceAddCommand(ParseResult parseResult)
        : base(parseResult)
    {
        (_fileOrDirectory, _allowedAppKinds) = PackageCommandParser.ProcessPathOptions(
            Definition.GetFileOption(),
            Definition.GetProjectOption(),
            Definition.GetProjectOrFileArgument(),
            parseResult);
    }

    public override int Execute()
    {
        using var projects = new ProjectCollection();
        bool interactive = _parseResult.GetValue(Definition.InteractiveOption);

        if (_allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuilder.IsValidEntryPointPath(_fileOrDirectory))
        {
            return ExecuteForFileBasedApp(projects, interactive);
        }

        if (!_allowedAppKinds.HasFlag(AppKinds.ProjectBased))
        {
            throw new GracefulException(CliCommandStrings.InvalidFilePath, _fileOrDirectory);
        }

        MsbuildProject msbuildProj = MsbuildProject.FromFileOrDirectory(
            projects,
            _fileOrDirectory,
            interactive);

        var frameworkString = _parseResult.GetValue(Definition.FrameworkOption);

        var arguments = _parseResult.GetValue(Definition.ProjectPathArgument).ToList().AsReadOnly();
        PathUtility.EnsureAllPathsExist(arguments,
            CliStrings.CouldNotFindProjectOrDirectory, true);
        List<MsbuildProject> refs = [.. arguments.Select((r) => MsbuildProject.FromFileOrDirectory(projects, r, interactive))];

        if (string.IsNullOrEmpty(frameworkString))
        {
            foreach (var tfm in msbuildProj.GetTargetFrameworks())
            {
                foreach (var @ref in refs)
                {
                    if (!@ref.CanWorkOnFramework(tfm))
                    {
                        Reporter.Error.Write(GetProjectNotCompatibleWithFrameworksDisplayString(
                                                 @ref,
                                                 msbuildProj.GetTargetFrameworks().Select((fx) => fx.GetShortFolderName())));
                        return 1;
                    }
                }
            }
        }
        else
        {
            var framework = NuGetFramework.Parse(frameworkString);
            if (!msbuildProj.IsTargetingFramework(framework))
            {
                Reporter.Error.WriteLine(string.Format(
                                             CliStrings.ProjectDoesNotTargetFramework,
                                             msbuildProj.ProjectRootElement.FullPath,
                                             frameworkString));
                return 1;
            }

            foreach (var @ref in refs)
            {
                if (!@ref.CanWorkOnFramework(framework))
                {
                    Reporter.Error.Write(GetProjectNotCompatibleWithFrameworksDisplayString(@ref, [frameworkString]));
                    return 1;
                }
            }
        }

        var relativePathReferences = refs.Select((r) =>
                                                    Path.GetRelativePath(
                                                        msbuildProj.ProjectDirectory,
                                                        r.ProjectRootElement.FullPath)).ToList();

        int numberOfAddedReferences = msbuildProj.AddProjectToProjectReferences(
            frameworkString,
            relativePathReferences);

        if (numberOfAddedReferences != 0)
        {
            msbuildProj.ProjectRootElement.Save();
        }

        return 0;
    }

    private int ExecuteForFileBasedApp(ProjectCollection projects, bool interactive)
    {
        if (!string.IsNullOrEmpty(_parseResult.GetValue(Definition.FrameworkOption)))
        {
            throw new GracefulException(CliCommandStrings.InvalidOptionForFileBasedApp, Definition.FrameworkOption.Name);
        }

        var arguments = _parseResult.GetValue(Definition.ProjectPathArgument).ToList().AsReadOnly();
        PathUtility.EnsureAllPathsExist(arguments, CliStrings.CouldNotFindProjectOrDirectory, true);

        var editor = new FileBasedAppReferenceEditor(_fileOrDirectory);
        var projectReferenceArguments = new List<string>();
        var fileBasedAppReferenceArguments = new List<string>();
        foreach (var argument in arguments)
        {
            if (VirtualProjectBuilder.IsValidEntryPointPath(argument))
            {
                fileBasedAppReferenceArguments.Add(argument);
            }
            else
            {
                projectReferenceArguments.Add(argument);
            }
        }

        List<MsbuildProject> refs = [.. projectReferenceArguments.Select((r) => MsbuildProject.FromFileOrDirectory(projects, r, interactive))];
        var targetFramework = NuGetFramework.Parse(VirtualProjectBuildingCommand.TargetFramework);
        foreach (var @ref in refs)
        {
            if (!@ref.CanWorkOnFramework(targetFramework))
            {
                Reporter.Error.Write(GetProjectNotCompatibleWithFrameworksDisplayString(@ref, [VirtualProjectBuildingCommand.TargetFramework]));
                return 1;
            }
        }

        int numberOfAddedReferences = editor.AddProjectReferences(
            refs.Zip(projectReferenceArguments, (reference, argument) =>
                (ProjectFilePath: reference.ProjectRootElement.FullPath,
                 DirectiveInclude: GetDirectiveInclude(argument, editor.EntryPointFileDirectory))));

        numberOfAddedReferences += editor.AddFileBasedAppReferences(fileBasedAppReferenceArguments.Select(argument =>
            (FilePath: Path.GetFullPath(argument),
             DirectiveInclude: GetDirectiveInclude(argument, editor.EntryPointFileDirectory))));

        if (numberOfAddedReferences != 0)
        {
            editor.Save();
        }

        return 0;

        static string GetDirectiveInclude(string argument, string projectDirectory)
        {
            return Path.GetRelativePath(projectDirectory, Path.GetFullPath(argument)).Replace('\\', '/');
        }
    }

    private static string GetProjectNotCompatibleWithFrameworksDisplayString(MsbuildProject project, IEnumerable<string> frameworksDisplayStrings)
    {
        var sb = new StringBuilder();
        sb.AppendLine(string.Format(CliStrings.ProjectNotCompatibleWithFrameworks, project.ProjectRootElement.FullPath));
        foreach (var tfm in frameworksDisplayStrings)
        {
            sb.AppendLine($"    - {tfm}");
        }

        return sb.ToString();
    }
}