Skip to content

Commit 86317d7

Browse files
edvilmeCopilot
andauthored
sln-add: Refactor logic for autogenerating solution folders (#48726)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7d160cc commit 86317d7

File tree

2 files changed

+112
-74
lines changed

2 files changed

+112
-74
lines changed

src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs

Lines changed: 97 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ namespace Microsoft.DotNet.Cli.Commands.Solution.Add;
1515

1616
internal class SolutionAddCommand : CommandBase
1717
{
18-
private static readonly string[] _defaultPlatforms = ["Any CPU", "x64", "x86"];
19-
private static readonly string[] _defaultBuildTypes = ["Debug", "Release"];
2018
private readonly string _fileOrDirectory;
2119
private readonly bool _inRoot;
2220
private readonly IReadOnlyCollection<string> _projects;
2321
private readonly string? _solutionFolderPath;
22+
private string _solutionFileFullPath = string.Empty;
2423

2524
private static string GetSolutionFolderPathWithForwardSlashes(string path)
2625
{
@@ -29,13 +28,21 @@ private static string GetSolutionFolderPathWithForwardSlashes(string path)
2928
return "/" + string.Join("/", PathUtility.GetPathWithDirectorySeparator(path).Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + "/";
3029
}
3130

31+
private static bool IsSolutionFolderPathInDirectoryScope(string relativePath)
32+
{
33+
return !string.IsNullOrWhiteSpace(relativePath)
34+
&& !Path.IsPathRooted(relativePath) // This means path is in a different volume
35+
&& !relativePath.StartsWith(".."); // This means path is outside the solution directory
36+
}
37+
3238
public SolutionAddCommand(ParseResult parseResult) : base(parseResult)
3339
{
3440
_fileOrDirectory = parseResult.GetValue(SolutionCommandParser.SlnArgument);
3541
_projects = (IReadOnlyCollection<string>)(parseResult.GetValue(SolutionAddCommandParser.ProjectPathArgument) ?? []);
3642
_inRoot = parseResult.GetValue(SolutionAddCommandParser.InRootOption);
3743
_solutionFolderPath = parseResult.GetValue(SolutionAddCommandParser.SolutionFolderOption);
3844
SolutionArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SolutionArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath);
45+
_solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
3946
}
4047

4148
public override int Execute()
@@ -44,115 +51,135 @@ public override int Execute()
4451
{
4552
throw new GracefulException(CliStrings.SpecifyAtLeastOneProjectToAdd);
4653
}
47-
string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
4854

49-
try
55+
// Get project paths from the command line arguments
56+
PathUtility.EnsureAllPathsExist(_projects, CliStrings.CouldNotFindProjectOrDirectory, true);
57+
58+
IEnumerable<string> fullProjectPaths = _projects.Select(project =>
5059
{
51-
PathUtility.EnsureAllPathsExist(_projects, CliStrings.CouldNotFindProjectOrDirectory, true);
52-
IEnumerable<string> fullProjectPaths = _projects.Select(project =>
53-
{
54-
var fullPath = Path.GetFullPath(project);
55-
return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath;
56-
});
57-
AddProjectsToSolutionAsync(solutionFileFullPath, fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult();
58-
return 0;
60+
var fullPath = Path.GetFullPath(project);
61+
return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath;
62+
});
63+
64+
// Add projects to the solution
65+
AddProjectsToSolutionAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult();
66+
return 0;
67+
}
68+
69+
private SolutionFolderModel? GenerateIntermediateSolutionFoldersForProjectPath(SolutionModel solution, string relativeProjectPath)
70+
{
71+
if (_inRoot)
72+
{
73+
return null;
5974
}
60-
catch (Exception ex) when (ex is not GracefulException)
75+
76+
string relativeSolutionFolderPath = string.Empty;
77+
78+
if (string.IsNullOrEmpty(_solutionFolderPath))
6179
{
80+
// Generate the solution folder path based on the project path
81+
relativeSolutionFolderPath = Path.GetDirectoryName(relativeProjectPath);
82+
83+
// If the project is in a folder with the same name as the project, we need to go up one level
84+
if (relativeSolutionFolderPath.Split(Path.DirectorySeparatorChar).LastOrDefault() == Path.GetFileNameWithoutExtension(relativeProjectPath))
85+
{
86+
relativeSolutionFolderPath = Path.Combine([.. relativeSolutionFolderPath.Split(Path.DirectorySeparatorChar).SkipLast(1)]);
87+
}
88+
89+
// If the generated path is outside the solution directory, we need to set it to empty
90+
if (!IsSolutionFolderPathInDirectoryScope(relativeSolutionFolderPath))
6291
{
63-
if (ex is SolutionException || ex.InnerException is SolutionException)
64-
{
65-
throw new GracefulException(CliStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
66-
}
67-
throw new GracefulException(ex.Message, ex);
92+
relativeSolutionFolderPath = string.Empty;
6893
}
6994
}
95+
else
96+
{
97+
// Use the provided solution folder path
98+
relativeSolutionFolderPath = _solutionFolderPath;
99+
}
100+
101+
return string.IsNullOrEmpty(relativeSolutionFolderPath)
102+
? null
103+
: solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(relativeSolutionFolderPath));
70104
}
71105

72-
private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
106+
private async Task AddProjectsToSolutionAsync(IEnumerable<string> projectPaths, CancellationToken cancellationToken)
73107
{
74-
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(solutionFileFullPath);
108+
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(_solutionFileFullPath);
75109
ISolutionSerializer serializer = solution.SerializerExtension.Serializer;
110+
76111
// set UTF8 BOM encoding for .sln
77112
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
78113
{
79114
solution.SerializerExtension = v12Serializer.CreateModelExtension(new()
80115
{
81116
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)
82117
});
118+
83119
// Set default configurations and platforms for sln file
84-
foreach (var platform in _defaultPlatforms)
120+
foreach (var platform in SlnFileFactory.DefaultPlatforms)
85121
{
86122
solution.AddPlatform(platform);
87123
}
88-
foreach (var buildType in _defaultBuildTypes)
124+
125+
foreach (var buildType in SlnFileFactory.DefaultBuildTypes)
89126
{
90127
solution.AddBuildType(buildType);
91128
}
92129
}
93130

94-
SolutionFolderModel? solutionFolder = !_inRoot && !string.IsNullOrEmpty(_solutionFolderPath)
95-
? solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(_solutionFolderPath))
96-
: null;
97-
98131
foreach (var projectPath in projectPaths)
99132
{
100-
string relativePath = Path.GetRelativePath(Path.GetDirectoryName(solutionFileFullPath), projectPath);
101-
// Add fallback solution folder if relative path does not contain `..`.
102-
string relativeSolutionFolder = relativePath.Split(Path.DirectorySeparatorChar).Any(p => p == "..")
103-
? string.Empty : Path.GetDirectoryName(relativePath);
104-
105-
if (!_inRoot && solutionFolder is null && !string.IsNullOrEmpty(relativeSolutionFolder))
106-
{
107-
if (relativeSolutionFolder.Split(Path.DirectorySeparatorChar).LastOrDefault() == Path.GetFileNameWithoutExtension(relativePath))
108-
{
109-
relativeSolutionFolder = Path.Combine([.. relativeSolutionFolder.Split(Path.DirectorySeparatorChar).SkipLast(1)]);
110-
}
111-
if (!string.IsNullOrEmpty(relativeSolutionFolder))
112-
{
113-
solutionFolder = solution.AddFolder(GetSolutionFolderPathWithForwardSlashes(relativeSolutionFolder));
114-
}
115-
}
116-
117-
try
118-
{
119-
AddProject(solution, relativePath, projectPath, solutionFolder, serializer);
120-
}
121-
catch (InvalidProjectFileException ex)
122-
{
123-
Reporter.Error.WriteLine(string.Format(CliStrings.InvalidProjectWithExceptionMessage, projectPath, ex.Message));
124-
}
125-
catch (SolutionArgumentException ex) when (solution.FindProject(relativePath) != null || ex.Type == SolutionErrorType.DuplicateProjectName)
126-
{
127-
Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, solutionFileFullPath, relativePath);
128-
}
133+
AddProject(solution, projectPath, serializer);
129134
}
130-
await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken);
135+
136+
await serializer.SaveAsync(_solutionFileFullPath, solution, cancellationToken);
131137
}
132138

133-
private static void AddProject(SolutionModel solution, string solutionRelativeProjectPath, string fullPath, SolutionFolderModel? solutionFolder, ISolutionSerializer serializer = null)
139+
private void AddProject(SolutionModel solution, string fullProjectPath, ISolutionSerializer serializer = null)
134140
{
141+
string solutionRelativeProjectPath = Path.GetRelativePath(Path.GetDirectoryName(_solutionFileFullPath), fullProjectPath);
142+
135143
// Open project instance to see if it is a valid project
136-
ProjectRootElement projectRootElement = ProjectRootElement.Open(fullPath);
144+
ProjectRootElement projectRootElement;
145+
try
146+
{
147+
projectRootElement = ProjectRootElement.Open(fullProjectPath);
148+
}
149+
catch (InvalidProjectFileException ex)
150+
{
151+
Reporter.Error.WriteLine(string.Format(CliStrings.InvalidProjectWithExceptionMessage, fullProjectPath, ex.Message));
152+
return;
153+
}
154+
137155
ProjectInstance projectInstance = new ProjectInstance(projectRootElement);
156+
157+
string projectTypeGuid = solution.ProjectTypes.FirstOrDefault(t => t.Extension == Path.GetExtension(fullProjectPath))?.ProjectTypeId.ToString()
158+
?? projectRootElement.GetProjectTypeGuid() ?? projectInstance.GetDefaultProjectTypeGuid();
159+
160+
// Generate the solution folder path based on the project path
161+
SolutionFolderModel? solutionFolder = GenerateIntermediateSolutionFoldersForProjectPath(solution, solutionRelativeProjectPath);
162+
138163
SolutionProjectModel project;
164+
139165
try
140166
{
141-
project = solution.AddProject(solutionRelativeProjectPath, null, solutionFolder);
167+
project = solution.AddProject(solutionRelativeProjectPath, projectTypeGuid, solutionFolder);
142168
}
143-
catch (SolutionArgumentException ex) when (ex.ParamName == "projectTypeName")
169+
catch (SolutionArgumentException ex) when (ex.Type == SolutionErrorType.InvalidProjectTypeReference)
144170
{
145-
// If guid is not identified by vs-solutionpersistence, check in project element itself
146-
var guid = projectRootElement.GetProjectTypeGuid() ?? projectInstance.GetDefaultProjectTypeGuid();
147-
if (string.IsNullOrEmpty(guid))
148-
{
149-
Reporter.Error.WriteLine(CliStrings.UnsupportedProjectType, fullPath);
150-
return;
151-
}
152-
project = solution.AddProject(solutionRelativeProjectPath, guid, solutionFolder);
171+
Reporter.Error.WriteLine(CliStrings.UnsupportedProjectType, fullProjectPath);
172+
return;
173+
}
174+
catch (SolutionArgumentException ex) when (ex.Type == SolutionErrorType.DuplicateProjectName || solution.FindProject(solutionRelativeProjectPath) is not null)
175+
{
176+
Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, _solutionFileFullPath, solutionRelativeProjectPath);
177+
return;
153178
}
179+
154180
// Add settings based on existing project instance
155181
string projectInstanceId = projectInstance.GetProjectId();
182+
156183
if (!string.IsNullOrEmpty(projectInstanceId) && serializer is ISolutionSerializer<SlnV12SerializerSettings>)
157184
{
158185
project.Id = new Guid(projectInstanceId);
@@ -164,7 +191,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
164191
foreach (var solutionPlatform in solution.Platforms)
165192
{
166193
var projectPlatform = projectInstancePlatforms.FirstOrDefault(
167-
platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault());
194+
platform => platform.Replace(" ", string.Empty) == solutionPlatform.Replace(" ", string.Empty), projectInstancePlatforms.FirstOrDefault());
168195
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.Platform, "*", solutionPlatform, projectPlatform));
169196
}
170197

@@ -174,6 +201,7 @@ private static void AddProject(SolutionModel solution, string solutionRelativePr
174201
buildType => buildType.Replace(" ", string.Empty) == solutionBuildType.Replace(" ", string.Empty), projectInstanceBuildTypes.FirstOrDefault());
175202
project.AddProjectConfigurationRule(new ConfigurationRule(BuildDimension.BuildType, solutionBuildType, "*", projectBuildType));
176203
}
204+
177205
Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, solutionRelativeProjectPath);
178206
}
179207
}

src/Cli/dotnet/SlnFileFactory.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Text.Json;
56
using Microsoft.DotNet.Cli.Utils;
67
using Microsoft.VisualStudio.SolutionPersistence;
@@ -11,6 +12,9 @@ namespace Microsoft.DotNet.Cli;
1112

1213
public static class SlnFileFactory
1314
{
15+
public static readonly string[] DefaultPlatforms = new[] { "Any CPU", "x64", "x86" };
16+
public static readonly string[] DefaultBuildTypes = new[] { "Debug", "Release" };
17+
1418
public static string GetSolutionFileFullPath(string slnFileOrDirectory, bool includeSolutionFilterFiles = false, bool includeSolutionXmlFiles = true)
1519
{
1620
// Throw error if slnFileOrDirectory is an invalid path
@@ -63,11 +67,17 @@ public static SolutionModel CreateFromFileOrDirectory(string fileOrDirectory, bo
6367
{
6468
return CreateFromFilteredSolutionFile(solutionPath);
6569
}
66-
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new GracefulException(
67-
CliStrings.CouldNotFindSolutionOrDirectory,
68-
solutionPath);
69-
70-
return serializer.OpenAsync(solutionPath, CancellationToken.None).Result;
70+
try
71+
{
72+
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath)!;
73+
return serializer.OpenAsync(solutionPath, CancellationToken.None).Result;
74+
}
75+
catch (Exception ex)
76+
{
77+
throw new GracefulException(
78+
CliStrings.InvalidSolutionFormatString,
79+
solutionPath, ex.Message);
80+
}
7181
}
7282

7383
public static SolutionModel CreateFromFilteredSolutionFile(string filteredSolutionPath)

0 commit comments

Comments
 (0)