Skip to content

Commit e1caf34

Browse files
authored
Refactor SlnFileFactory + slnf (#46002)
1 parent 6bd63ed commit e1caf34

File tree

10 files changed

+128
-109
lines changed

10 files changed

+128
-109
lines changed

src/Cli/dotnet/ReleasePropertyProjectLocator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public IEnumerable<string> GetCustomDefaultConfigurationValueIfSpecified()
155155
SolutionModel sln;
156156
try
157157
{
158-
sln = SlnFileFactory.CreateFromFileOrDirectory(slnFullPath, false);
158+
sln = SlnFileFactory.CreateFromFileOrDirectory(slnFullPath, false, false);
159159
}
160160
catch (GracefulException)
161161
{

src/Cli/dotnet/SlnFileFactory.cs

Lines changed: 80 additions & 55 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.Text.Json;
45
using Microsoft.DotNet.Cli.Utils;
56
using Microsoft.VisualStudio.SolutionPersistence;
67
using Microsoft.VisualStudio.SolutionPersistence.Model;
@@ -10,6 +11,40 @@ namespace Microsoft.DotNet.Tools.Common
1011
{
1112
public static class SlnFileFactory
1213
{
14+
public static string GetSolutionFileFullPath(string slnFileOrDirectory, bool includeSolutionFilterFiles = false, bool includeSolutionXmlFiles = true)
15+
{
16+
// Throw error if slnFileOrDirectory is an invalid path
17+
if (string.IsNullOrWhiteSpace(slnFileOrDirectory) || slnFileOrDirectory.IndexOfAny(Path.GetInvalidPathChars()) != -1)
18+
{
19+
throw new GracefulException(CommonLocalizableStrings.CouldNotFindSolutionOrDirectory);
20+
}
21+
if (File.Exists(slnFileOrDirectory))
22+
{
23+
return Path.GetFullPath(slnFileOrDirectory);
24+
}
25+
if (Directory.Exists(slnFileOrDirectory))
26+
{
27+
string[] files = ListSolutionFilesInDirectory(slnFileOrDirectory, includeSolutionFilterFiles, includeSolutionXmlFiles);
28+
if (files.Length == 0)
29+
{
30+
throw new GracefulException(
31+
CommonLocalizableStrings.CouldNotFindSolutionIn,
32+
slnFileOrDirectory);
33+
}
34+
if (files.Length > 1)
35+
{
36+
throw new GracefulException(
37+
CommonLocalizableStrings.MoreThanOneSolutionInDirectory,
38+
slnFileOrDirectory);
39+
}
40+
return Path.GetFullPath(files.Single());
41+
}
42+
throw new GracefulException(
43+
CommonLocalizableStrings.CouldNotFindSolutionOrDirectory,
44+
slnFileOrDirectory);
45+
}
46+
47+
1348
public static string[] ListSolutionFilesInDirectory(string directory, bool includeSolutionFilterFiles = false, bool includeSolutionXmlFiles = true)
1449
{
1550
return [
@@ -19,87 +54,77 @@ public static string[] ListSolutionFilesInDirectory(string directory, bool inclu
1954
];
2055
}
2156

22-
public static SolutionModel CreateFromFileOrDirectory(string fileOrDirectory, bool includeSolutionXmlFiles = true)
57+
public static SolutionModel CreateFromFileOrDirectory(string fileOrDirectory, bool includeSolutionFilterFiles = false, bool includeSolutionXmlFiles = true)
2358
{
24-
if (File.Exists(fileOrDirectory))
25-
{
26-
return FromFile(fileOrDirectory);
27-
}
28-
else
29-
{
30-
return FromDirectory(fileOrDirectory, includeSolutionXmlFiles);
31-
}
32-
}
59+
string solutionPath = GetSolutionFileFullPath(fileOrDirectory, includeSolutionFilterFiles, includeSolutionXmlFiles);
3360

34-
private static SolutionModel FromFile(string solutionPath)
35-
{
36-
SolutionModel slnFile = null;
37-
try
61+
if (solutionPath.HasExtension(".slnf"))
3862
{
39-
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new GracefulException(
63+
return CreateFromFilteredSolutionFile(solutionPath);
64+
}
65+
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath) ?? throw new GracefulException(
4066
CommonLocalizableStrings.CouldNotFindSolutionOrDirectory,
4167
solutionPath);
4268

43-
slnFile = serializer.OpenAsync(solutionPath, CancellationToken.None).Result;
44-
}
45-
catch (SolutionException e)
46-
{
47-
throw new GracefulException(
48-
CommonLocalizableStrings.InvalidSolutionFormatString,
49-
solutionPath,
50-
e.Message);
51-
}
52-
return slnFile;
69+
return serializer.OpenAsync(solutionPath, CancellationToken.None).Result;
5370
}
5471

55-
private static SolutionModel FromDirectory(string solutionDirectory, bool includeSolutionXmlFiles)
72+
public static SolutionModel CreateFromFilteredSolutionFile(string filteredSolutionPath)
5673
{
57-
DirectoryInfo dir;
74+
JsonDocument jsonDocument;
75+
JsonElement jsonElement;
76+
JsonElement filteredSolutionJsonElement;
77+
string originalSolutionPath;
78+
string originalSolutionPathAbsolute;
79+
string[] filteredSolutionProjectPaths;
80+
5881
try
5982
{
60-
dir = new DirectoryInfo(solutionDirectory);
61-
if (!dir.Exists)
83+
jsonDocument = JsonDocument.Parse(File.ReadAllText(filteredSolutionPath));
84+
jsonElement = jsonDocument.RootElement;
85+
filteredSolutionJsonElement = jsonElement.GetProperty("solution");
86+
originalSolutionPath = filteredSolutionJsonElement.GetProperty("path").GetString();
87+
originalSolutionPathAbsolute = Path.GetFullPath(originalSolutionPath, Path.GetDirectoryName(filteredSolutionPath));
88+
if (!File.Exists(originalSolutionPathAbsolute))
6289
{
63-
throw new GracefulException(
64-
CommonLocalizableStrings.CouldNotFindSolutionOrDirectory,
65-
solutionDirectory);
90+
throw new Exception();
6691
}
92+
filteredSolutionProjectPaths = filteredSolutionJsonElement.GetProperty("projects")
93+
.EnumerateArray()
94+
.Select(project => project.GetString())
95+
.ToArray();
6796
}
68-
catch (ArgumentException)
69-
{
97+
catch (Exception ex) {
7098
throw new GracefulException(
71-
CommonLocalizableStrings.CouldNotFindSolutionOrDirectory,
72-
solutionDirectory);
99+
CommonLocalizableStrings.InvalidSolutionFormatString,
100+
filteredSolutionPath, ex.Message);
73101
}
74102

75-
FileInfo[] files = [
76-
..dir.GetFiles("*.sln"),
77-
..(includeSolutionXmlFiles ? dir.GetFiles(".slnx") : [])
78-
];
103+
SolutionModel filteredSolution = new SolutionModel();
104+
SolutionModel originalSolution = CreateFromFileOrDirectory(originalSolutionPathAbsolute);
79105

80-
if (files.Length == 0)
106+
foreach (var platform in originalSolution.Platforms)
81107
{
82-
throw new GracefulException(
83-
CommonLocalizableStrings.CouldNotFindSolutionIn,
84-
solutionDirectory);
108+
filteredSolution.AddPlatform(platform);
85109
}
86-
87-
if (files.Length > 1)
110+
foreach (var buildType in originalSolution.BuildTypes)
88111
{
89-
throw new GracefulException(
90-
CommonLocalizableStrings.MoreThanOneSolutionInDirectory,
91-
solutionDirectory);
112+
filteredSolution.AddBuildType(buildType);
92113
}
93114

94-
FileInfo solutionFile = files.Single();
95-
if (!solutionFile.Exists)
115+
foreach (string path in filteredSolutionProjectPaths)
96116
{
97-
throw new GracefulException(
98-
CommonLocalizableStrings.CouldNotFindSolutionIn,
99-
solutionDirectory);
117+
// Normalize path to use correct directory separator
118+
string normalizedPath = path.Replace('\\', Path.DirectorySeparatorChar);
119+
120+
SolutionProjectModel project = originalSolution.FindProject(normalizedPath) ?? throw new GracefulException(
121+
CommonLocalizableStrings.ProjectNotFoundInTheSolution,
122+
normalizedPath,
123+
originalSolutionPath);
124+
filteredSolution.AddProject(project.FilePath, project.Type, project.Parent is null ? null : filteredSolution.AddFolder(project.Parent.Path));
100125
}
101126

102-
return FromFile(solutionFile.FullName);
127+
return filteredSolution;
103128
}
104129
}
105130
}

src/Cli/dotnet/commands/dotnet-sln/SlnCommandParser.cs

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,6 @@ private static CliCommand ConstructCommand()
4747

4848
return command;
4949
}
50-
51-
internal static string GetSlnFileFullPath(string slnFileOrDirectory)
52-
{
53-
if (File.Exists(slnFileOrDirectory))
54-
{
55-
return Path.GetFullPath(slnFileOrDirectory);
56-
}
57-
if (Directory.Exists(slnFileOrDirectory))
58-
{
59-
string[] files = [
60-
..Directory.GetFiles(slnFileOrDirectory, "*.sln", SearchOption.TopDirectoryOnly),
61-
..Directory.GetFiles(slnFileOrDirectory, "*.slnx", SearchOption.TopDirectoryOnly)];
62-
if (files.Length == 0)
63-
{
64-
throw new GracefulException(CommonLocalizableStrings.CouldNotFindSolutionIn, slnFileOrDirectory);
65-
}
66-
if (files.Length > 1)
67-
{
68-
throw new GracefulException(CommonLocalizableStrings.MoreThanOneSolutionInDirectory, slnFileOrDirectory);
69-
}
70-
return Path.GetFullPath(files.Single());
71-
}
72-
throw new GracefulException(CommonLocalizableStrings.CouldNotFindSolutionOrDirectory, slnFileOrDirectory);
73-
}
74-
75-
internal static ISolutionSerializer GetSolutionSerializer(string solutionFilePath)
76-
{
77-
ISolutionSerializer? serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
78-
if (serializer is null)
79-
{
80-
throw new GracefulException(LocalizableStrings.SerializerNotFound, solutionFilePath);
81-
}
82-
return serializer;
83-
}
8450
}
51+
8552
}

src/Cli/dotnet/commands/dotnet-sln/add/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public override int Execute()
4646
{
4747
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd);
4848
}
49-
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);
49+
string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
5050

5151
try
5252
{
@@ -73,8 +73,8 @@ public override int Execute()
7373

7474
private async Task AddProjectsToSolutionAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
7575
{
76-
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
77-
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
76+
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(solutionFileFullPath);
77+
ISolutionSerializer serializer = solution.SerializerExtension.Serializer;
7878
// set UTF8 BOM encoding for .sln
7979
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
8080
{

src/Cli/dotnet/commands/dotnet-sln/list/Program.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.CommandLine;
55
using Microsoft.DotNet.Cli;
66
using Microsoft.DotNet.Cli.Utils;
7+
using Microsoft.DotNet.Tools.Common;
78
using Microsoft.VisualStudio.SolutionPersistence;
89
using Microsoft.VisualStudio.SolutionPersistence.Model;
910
using CommandLocalizableStrings = Microsoft.DotNet.Tools.CommonLocalizableStrings;
@@ -24,10 +25,10 @@ public ListProjectsInSolutionCommand(
2425

2526
public override int Execute()
2627
{
27-
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);
28+
string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory, includeSolutionFilterFiles: true);
2829
try
2930
{
30-
ListAllProjectsAsync(solutionFileFullPath, CancellationToken.None).Wait();
31+
ListAllProjectsAsync(solutionFileFullPath);
3132
return 0;
3233
}
3334
catch (Exception ex)
@@ -36,10 +37,9 @@ public override int Execute()
3637
}
3738
}
3839

39-
private async Task ListAllProjectsAsync(string solutionFileFullPath, CancellationToken cancellationToken)
40+
private void ListAllProjectsAsync(string solutionFileFullPath)
4041
{
41-
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
42-
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
42+
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(solutionFileFullPath);
4343
string[] paths;
4444
if (_displaySolutionFolders)
4545
{

src/Cli/dotnet/commands/dotnet-sln/migrate/SlnMigrateCommand.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public SlnMigrateCommand(
3333

3434
public override int Execute()
3535
{
36-
string slnFileFullPath = SlnCommandParser.GetSlnFileFullPath(_slnFileOrDirectory);
36+
string slnFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_slnFileOrDirectory);
3737
if (slnFileFullPath.HasExtension(".slnx"))
3838
{
3939
throw new GracefulException(LocalizableStrings.CannotMigrateSlnx);
@@ -50,8 +50,7 @@ public override int Execute()
5050

5151
private async Task ConvertToSlnxAsync(string filePath, string slnxFilePath, CancellationToken cancellationToken)
5252
{
53-
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(filePath);
54-
SolutionModel solution = await serializer.OpenAsync(filePath, cancellationToken);
53+
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(filePath);
5554
await SolutionSerializers.SlnXml.SaveAsync(slnxFilePath, solution, cancellationToken);
5655
_reporter.WriteLine(LocalizableStrings.SlnxGenerated, slnxFilePath);
5756
}

src/Cli/dotnet/commands/dotnet-sln/remove/Program.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public RemoveProjectFromSolutionCommand(ParseResult parseResult) : base(parseRes
3030

3131
public override int Execute()
3232
{
33-
string solutionFileFullPath = SlnCommandParser.GetSlnFileFullPath(_fileOrDirectory);
33+
string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
3434
if (_projects.Count == 0)
3535
{
3636
throw new GracefulException(CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove);
@@ -47,7 +47,7 @@ public override int Execute()
4747
? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName
4848
: fullPath);
4949
});
50-
RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).Wait();
50+
RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult();
5151
return 0;
5252
}
5353
catch (Exception ex) when (ex is not GracefulException)
@@ -56,18 +56,14 @@ public override int Execute()
5656
{
5757
throw new GracefulException(CommonLocalizableStrings.InvalidSolutionFormatString, solutionFileFullPath, ex.Message);
5858
}
59-
if (ex.InnerException is GracefulException)
60-
{
61-
throw ex.InnerException;
62-
}
6359
throw new GracefulException(ex.Message, ex);
6460
}
6561
}
6662

6763
private async Task RemoveProjectsAsync(string solutionFileFullPath, IEnumerable<string> projectPaths, CancellationToken cancellationToken)
6864
{
69-
ISolutionSerializer serializer = SlnCommandParser.GetSolutionSerializer(solutionFileFullPath);
70-
SolutionModel solution = await serializer.OpenAsync(solutionFileFullPath, cancellationToken);
65+
SolutionModel solution = SlnFileFactory.CreateFromFileOrDirectory(solutionFileFullPath);
66+
ISolutionSerializer serializer = solution.SerializerExtension.Serializer;
7167

7268
// set UTF-8 BOM encoding for .sln
7369
if (serializer is ISolutionSerializer<SlnV12SerializerSettings> v12Serializer)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"solution": {
3+
"path": "./App.slnx",
4+
"projects": [
5+
"src\\App\\App.csproj"
6+
]
7+
}
8+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<Solution>
2+
<Project Path="src/App/App.csproj" />
3+
<Project Path="src/OtherApp/OtherApp.csproj" />
4+
</Solution>

test/dotnet-sln.Tests/GivenDotnetSlnList.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,25 @@ public void WhenProjectsInSolutionFoldersPresentInTheSolutionItListsSolutionFold
260260
cmd.Should().Pass();
261261
cmd.StdOut.Should().ContainAll(expectedOutput);
262262
}
263+
264+
[Theory]
265+
[InlineData("sln")]
266+
[InlineData("solution")]
267+
public void WhenSolutionFilterIsPassedItListsProjectsMatching(string solutionCommand)
268+
{
269+
string[] expectedOutput = { $"{CommandLocalizableStrings.ProjectsHeader}",
270+
$"{new string('-', CommandLocalizableStrings.ProjectsHeader.Length)}",
271+
$"{Path.Combine("src", "App", "App.csproj")}" };
272+
var projectDirectory = _testAssetsManager
273+
.CopyTestAsset("TestAppWithSlnxAndSolutionFilters", identifier: "GivenDotnetSlnList-Filter")
274+
.WithSource()
275+
.Path;
276+
277+
var cmd = new DotnetCommand(Log)
278+
.WithWorkingDirectory(projectDirectory)
279+
.Execute(solutionCommand, "App.slnf", "list");
280+
cmd.Should().Pass();
281+
cmd.StdOut.Should().ContainAll(expectedOutput);
282+
}
263283
}
264284
}

0 commit comments

Comments
 (0)