File: Commands\Solution\Remove\SolutionRemoveCommand.cs
Web Access
Project: ..\..\..\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.DotNet.Cli.Utils;
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;
 
namespace Microsoft.DotNet.Cli.Commands.Solution.Remove;
 
internal class SolutionRemoveCommand : CommandBase
{
    private readonly string _fileOrDirectory;
    private readonly IReadOnlyCollection<string> _projects;
 
    public SolutionRemoveCommand(ParseResult parseResult) : base(parseResult)
    {
        _fileOrDirectory = parseResult.GetValue(SolutionCommandParser.SlnArgument);
 
        _projects = (parseResult.GetValue(SolutionRemoveCommandParser.ProjectPathArgument) ?? []).ToList().AsReadOnly();
 
        SolutionArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SolutionArgumentValidator.CommandType.Remove);
    }
 
    public override int Execute()
    {
        string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
        if (_projects.Count == 0)
        {
            throw new GracefulException(CliStrings.SpecifyAtLeastOneProjectToRemove);
        }
 
        try
        {
            var relativeProjectPaths = _projects
                .Select(p => Path.GetFullPath(p))
                .Select(p => Path.GetRelativePath(
                    Path.GetDirectoryName(solutionFileFullPath),
                    Directory.Exists(p)
                        ? MsbuildProject.GetProjectFileFromDirectory(p).FullName
                        : p));
 
            RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult();
            return 0;
        }
        catch (Exception ex) when (ex is not GracefulException)
        {
            if (ex is SolutionException || ex.InnerException is SolutionException)
            {
                throw new GracefulException(CliStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
            }
            throw new GracefulException(ex.Message, ex);
        }
    }
 
    private static async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
    {
        SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(solutionFileFullPath);
        ISolutionSerializer serializer = solution.SerializerExtension.Serializer;
 
        // set UTF-8 BOM encoding for .sln
        if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
        {
            solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
            {
                Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
            });
        }
 
        foreach (var projectPath in projectPaths)
        {
            var project = solution.FindProject(projectPath);
            // If the project is not found, try to find it by name without extension
            if (project is null && !Path.HasExtension(projectPath))
            {
                var projectsMatchByName = solution.SolutionProjects.Where(p => Path.GetFileNameWithoutExtension(p.DisplayName).Equals(projectPath));
                project = projectsMatchByName.Count() == 1 ? projectsMatchByName.First() : null;
            }
            // If project is still not found, print error
            if (project is null)
            {
                Reporter.Output.WriteLine(CliStrings.ProjectNotFoundInTheSolution, projectPath);
            }
            // If project is found, remove it
            else
            {
                solution.RemoveProject(project);
                Reporter.Output.WriteLine(CliStrings.ProjectRemovedFromTheSolution, projectPath);
            }
        }
 
        for (int i = 0; i < solution.SolutionFolders.Count; i++)
        {
            var folder = solution.SolutionFolders[i];
            int nonFolderDescendants = 0;
            Stack<SolutionFolderModel> stack = new();
            stack.Push(folder);
 
            while (stack.Count > 0)
            {
                var current = stack.Pop();
 
                nonFolderDescendants += current.Files?.Count ?? 0;
                foreach (var child in solution.SolutionItems)
                {
                    if (child is { Parent: var parent } && parent == current)
                    {
                        if (child is SolutionFolderModel childFolder)
                        {
                            stack.Push(childFolder);
                        }
                        else
                        {
                            nonFolderDescendants++;
                        }
                    }
                }
            }
 
            if (nonFolderDescendants == 0)
            {
                solution.RemoveFolder(folder);
                // After removal, adjust index and continue to avoid skipping folders after removal
                i--; 
            }
        }
 
        await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
    }
}