diff --git a/Directory.Packages.props b/Directory.Packages.props
index a8ab9b1..65c031f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,6 +11,7 @@
9.0.0
8.0.0
6.0.0
+ 1.0.52
@@ -25,6 +26,7 @@
+
diff --git a/src/Microsoft.VisualStudio.SlnGen.UnitTests/Microsoft.VisualStudio.SlnGen.UnitTests.csproj b/src/Microsoft.VisualStudio.SlnGen.UnitTests/Microsoft.VisualStudio.SlnGen.UnitTests.csproj
index 3532f6a..c7f049e 100644
--- a/src/Microsoft.VisualStudio.SlnGen.UnitTests/Microsoft.VisualStudio.SlnGen.UnitTests.csproj
+++ b/src/Microsoft.VisualStudio.SlnGen.UnitTests/Microsoft.VisualStudio.SlnGen.UnitTests.csproj
@@ -1,31 +1,32 @@
-
-
- net472;net8.0;net9.0
- $(TargetFrameworks);net10.0
- false
- $(NoWarn);SA1600
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ net472;net8.0;net9.0
+ $(TargetFrameworks);net10.0
+ false
+ $(NoWarn);SA1600
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs b/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs
index 861158c..5986079 100644
--- a/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs
+++ b/src/Microsoft.VisualStudio.SlnGen.UnitTests/SlnFileTests.cs
@@ -6,6 +6,9 @@
using Microsoft.Build.Evaluation;
using Microsoft.Build.Utilities.ProjectCreation;
using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.VisualStudio.SolutionPersistence;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
using Shouldly;
using System;
using System.Collections.Generic;
@@ -13,6 +16,8 @@
using System.Linq;
using System.Reflection;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using Xunit;
namespace Microsoft.VisualStudio.SlnGen.UnitTests
@@ -108,8 +113,9 @@ public void CustomConfigurationAndPlatforms()
slnFile.AddProjects(new[] { projectA, projectB, projectC, projectD, projectE, projectF, projectG });
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -195,8 +201,9 @@ public void CustomConfigurationAndPlatformsWithAlwaysBuildDisabled()
slnFile.AddProjects(new[] { projectA, projectB, projectC, projectD, projectE });
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
- slnFile.Save(solutionFilePath, useFolders: false, alwaysBuild: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false, alwaysBuild: false);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -234,8 +241,9 @@ public void CustomConfigurationAndPlatforms_IgnoresInvalidValues()
slnFile.AddProjects(new[] { project });
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -316,8 +324,9 @@ public void CustomConfigurationAndPlatforms_MapsAnyCPU()
slnFile.AddProjects(new[] { projectA, projectB, projectC, projectD });
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -334,6 +343,7 @@ public void CustomConfigurationAndPlatforms_MapsAnyCPU()
public void ExistingSolutionIsReused()
{
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
Guid projectGuid = Guid.Parse("7BE5A5CA-169D-4955-AB4D-EDDE662F4AE5");
@@ -357,9 +367,9 @@ public void ExistingSolutionIsReused()
slnFile.AddProjects(new[] { project });
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
- SlnFile.TryParseExistingSolution(solutionFilePath, out Guid solutionGuid, out _).ShouldBeTrue();
+ SlnFile.TryParseExistingSolution(solutionFilePath, out Guid solutionGuid, out _, out serializer).ShouldBeTrue();
solutionGuid.ShouldBe(slnFile.SolutionGuid);
@@ -373,10 +383,13 @@ public void ExistingSolutionIsReused()
[Fact]
public void MultipleProjects()
{
+ string projectAPath = Path.GetRandomFileName();
+ string projectBPath = Path.GetRandomFileName();
+
SlnProject projectA = new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectA",
+ FullPath = Path.Combine(TestRootPath, projectAPath),
+ Name = Path.GetFileNameWithoutExtension(projectAPath),
ProjectGuid = new Guid("C95D800E-F016-4167-8E1B-1D3FF94CE2E2"),
ProjectTypeGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"),
IsMainProject = true,
@@ -384,8 +397,8 @@ public void MultipleProjects()
SlnProject projectB = new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectB",
+ FullPath = Path.Combine(TestRootPath, projectBPath),
+ Name = Path.GetFileNameWithoutExtension(projectBPath),
ProjectGuid = new Guid("EAD108BE-AC70-41E6-A8C3-450C545FDC0E"),
ProjectTypeGuid = new Guid("F38341C3-343F-421A-AE68-94CD9ADCD32F"),
};
@@ -396,27 +409,31 @@ public void MultipleProjects()
[Fact]
public void NoFolders()
{
+ string projectAPath = Path.GetRandomFileName();
+ string projectBPath = Path.GetRandomFileName();
+ string projectCPath = Path.GetRandomFileName();
+
SlnProject[] projects =
{
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectA",
+ FullPath = Path.Combine(TestRootPath, projectAPath),
+ Name = Path.GetFileNameWithoutExtension(projectAPath),
ProjectGuid = new Guid("C95D800E-F016-4167-8E1B-1D3FF94CE2E2"),
ProjectTypeGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"),
IsMainProject = true,
},
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectB",
+ FullPath = Path.Combine(TestRootPath, projectBPath),
+ Name = Path.GetFileNameWithoutExtension(projectBPath),
ProjectGuid = new Guid("EAD108BE-AC70-41E6-A8C3-450C545FDC0E"),
ProjectTypeGuid = new Guid("F38341C3-343F-421A-AE68-94CD9ADCD32F"),
},
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectC",
+ FullPath = Path.Combine(TestRootPath, projectCPath),
+ Name = Path.GetFileNameWithoutExtension(projectCPath),
ProjectGuid = new Guid("EDD837F8-48ED-45E1-BC77-6387EC6466AC"),
ProjectTypeGuid = new Guid("7C203CD8-314C-4358-AD5C-66152E899EAF"),
},
@@ -428,7 +445,8 @@ public void NoFolders()
[Fact]
public void PathsWorkForAllDirectorySeparatorChars()
{
- const string solutionText = @"Microsoft Visual Studio Solution File, Format Version 12.00
+ const string solutionText = @"
+Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""ProjectA"", ""ProjectA\ProjectA.csproj"", ""{E859E866-96F9-474E-A1EA-6539385AD236}""
EndProject
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""ProjectB"", ""ProjectB\ProjectB.csproj"", ""{893607F9-C204-4CB2-8BF2-1F71B4198CD2}""
@@ -495,7 +513,8 @@ public void PathsWorkForAllDirectorySeparatorChars()
[Fact]
public void ProjectsNotBuildable()
{
- const string solutionText = @"Microsoft Visual Studio Solution File, Format Version 12.00
+ const string solutionText = @"
+Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""ProjectA"", ""ProjectA\ProjectA.csproj"", ""{E859E866-96F9-474E-A1EA-6539385AD236}""
EndProject
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""ProjectB"", ""ProjectB\ProjectB.csproj"", ""{893607F9-C204-4CB2-8BF2-1F71B4198CD2}""
@@ -597,8 +616,8 @@ public void TestSlnGenFoldersPropertyToEnableFolderCreation()
string solutionFilePath = GetSolutionFilePath(new Project[] { projectA, projectB, projectC });
string contents = File.ReadAllText(solutionFilePath);
- contents.ShouldContain("\"..\\testB\",");
- contents.ShouldContain("\"..\\testC\",");
+ contents.ShouldContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testB\", \"testB\",");
+ contents.ShouldContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testC\", \"testC\",");
}
[Fact]
@@ -618,8 +637,8 @@ public void TestSlnGenFoldersPropertyToDisableFolderCreation()
string solutionFilePath = GetSolutionFilePath(new Project[] { projectA, projectB, projectC });
string contents = File.ReadAllText(solutionFilePath);
- contents.ShouldNotContain("\"..\\testB\",");
- contents.ShouldNotContain("\"..\\testC\",");
+ contents.ShouldNotContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testB\", \"testB\",");
+ contents.ShouldNotContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testC\", \"testC\",");
}
[Fact]
@@ -631,8 +650,8 @@ public void TestNoFolderCreation()
string solutionFilePath = GetSolutionFilePath(new Project[] { projectA, projectB, projectC });
string contents = File.ReadAllText(solutionFilePath);
- contents.ShouldNotContain("\"..\\testB\",");
- contents.ShouldNotContain("\"..\\testC\",");
+ contents.ShouldNotContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testB\", \"testB\",");
+ contents.ShouldNotContain("Project(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"testC\", \"testC\",");
}
[Fact]
@@ -668,7 +687,7 @@ public void ProjectConfigurationPlatformOrderingSameAsProjects()
{
FullPath = Path.Combine(TestRootPath, "B", "B.csproj"),
Name = "B",
- ProjectGuid = new Guid("0CCA75AE-ED20-431E-8853-B9F54333E87A"),
+ ProjectGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"),
ProjectTypeGuid = new Guid(projectTypeGuid),
Configurations = new[]
{
@@ -698,19 +717,21 @@ public void ProjectConfigurationPlatformOrderingSameAsProjects()
},
});
- string path = Path.GetTempFileName();
+ string path = Path.ChangeExtension(Path.GetTempFileName(), ".sln");
- slnFile.Save(path, useFolders: false);
+ slnFile.CreateSolutionDirectory(path);
+ slnFile.Save(SolutionSerializers.GetSerializerByMoniker(path), path, useFolders: false);
string directoryName = new DirectoryInfo(TestRootPath).Name;
File.ReadAllText(path).ShouldBe(
- $@"Microsoft Visual Studio Solution File, Format Version 12.00
+ $@"
+Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{{7E0F1516-6200-48BD-83FC-3EFA3AB4A574}}"") = ""C"", ""{directoryName}\C\C.csproj"", ""{{0CCA75AE-ED20-431E-8853-B9F54333E87A}}""
EndProject
Project(""{{7E0F1516-6200-48BD-83FC-3EFA3AB4A574}}"") = ""A"", ""{directoryName}\A\A.csproj"", ""{{D744C26F-1CCB-456A-B490-CEB39334051B}}""
EndProject
-Project(""{{7E0F1516-6200-48BD-83FC-3EFA3AB4A574}}"") = ""B"", ""{directoryName}\B\B.csproj"", ""{{0CCA75AE-ED20-431E-8853-B9F54333E87A}}""
+Project(""{{7E0F1516-6200-48BD-83FC-3EFA3AB4A574}}"") = ""B"", ""{directoryName}\B\B.csproj"", ""{{88152E7E-47E3-45C8-B5D3-DDB15B2F0435}}""
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -726,10 +747,10 @@ public void ProjectConfigurationPlatformOrderingSameAsProjects()
{{D744C26F-1CCB-456A-B490-CEB39334051B}}.Debug|Any CPU.Build.0 = Debug|Any CPU
{{D744C26F-1CCB-456A-B490-CEB39334051B}}.Release|Any CPU.ActiveCfg = Release|Any CPU
{{D744C26F-1CCB-456A-B490-CEB39334051B}}.Release|Any CPU.Build.0 = Release|Any CPU
- {{0CCA75AE-ED20-431E-8853-B9F54333E87A}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {{0CCA75AE-ED20-431E-8853-B9F54333E87A}}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {{0CCA75AE-ED20-431E-8853-B9F54333E87A}}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {{0CCA75AE-ED20-431E-8853-B9F54333E87A}}.Release|Any CPU.Build.0 = Release|Any CPU
+ {{88152E7E-47E3-45C8-B5D3-DDB15B2F0435}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {{88152E7E-47E3-45C8-B5D3-DDB15B2F0435}}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {{88152E7E-47E3-45C8-B5D3-DDB15B2F0435}}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {{88152E7E-47E3-45C8-B5D3-DDB15B2F0435}}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -777,12 +798,14 @@ public void ProjectSolutionFolders()
string[] solutionItems = new[] { Path.Combine(root, "SubFolder1", solutionItem1Name) };
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile();
slnFile.AddProjects(projects, new Dictionary(), projects[1].FullPath);
slnFile.AddSolutionItems(solutionItems);
- slnFile.Save(solutionFilePath, useFolders: false);
+
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
SolutionFile s = SolutionFile.Parse(solutionFilePath);
@@ -814,11 +837,12 @@ public void SaveToCustomLocationCreatesDirectory()
directoryInfo.Exists.ShouldBeFalse();
- string fullPath = Path.Combine(directoryInfo.FullName, Path.GetRandomFileName());
+ string fullPath = Path.Combine(directoryInfo.FullName, GetTempFileName(".sln"));
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(fullPath);
SlnFile slnFile = new SlnFile();
- slnFile.Save(fullPath, useFolders: false);
+ slnFile.Save(serializer, fullPath, useFolders: false);
File.Exists(fullPath).ShouldBeTrue();
}
@@ -872,10 +896,12 @@ public void SharedProject()
[Fact]
public void SingleProject()
{
+ string filePath = Path.GetRandomFileName();
+ string fileName = Path.GetFileNameWithoutExtension(filePath);
SlnProject projectA = new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectA",
+ FullPath = Path.Combine(TestRootPath, filePath),
+ Name = fileName,
ProjectGuid = new Guid("C95D800E-F016-4167-8E1B-1D3FF94CE2E2"),
ProjectTypeGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"),
IsMainProject = true,
@@ -887,7 +913,7 @@ public void SingleProject()
[Fact]
public void TryParseExistingSolution()
{
- FileInfo solutionFilePath = new FileInfo(GetTempFileName());
+ FileInfo solutionFilePath = new FileInfo(GetTempFileName(".sln"));
Dictionary projects = new Dictionary
{
@@ -897,8 +923,8 @@ public void TryParseExistingSolution()
Dictionary folders = new Dictionary
{
- [@"FolderA"] = new Guid("9C915FE4-72A5-4368-8979-32B3983E6041"),
- [@"FolderB"] = new Guid("D3A9F802-38CC-4F8D-8DE9-8DF9C8B7EADC"),
+ ["//FolderA//"] = new Guid("9C915FE4-72A5-4368-8979-32B3983E6041"),
+ ["//FolderB//"] = new Guid("D3A9F802-38CC-4F8D-8DE9-8DF9C8B7EADC"),
};
Dictionary projectFiles = projects.ToDictionary(i => new FileInfo(Path.Combine(solutionFilePath.DirectoryName!, i.Key)), i => i.Value);
@@ -952,7 +978,7 @@ public void TryParseExistingSolution()
EndGlobalSection
EndGlobal
");
- SlnFile.TryParseExistingSolution(solutionFilePath.FullName, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath).ShouldBeTrue();
+ SlnFile.TryParseExistingSolution(solutionFilePath.FullName, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath, out _).ShouldBeTrue();
solutionGuid.ShouldBe(Guid.Parse("CFFC4187-96EE-4465-B5B3-0BAFD3C14BB6"));
@@ -962,28 +988,32 @@ public void TryParseExistingSolution()
[Fact]
public void WithFolders()
{
+ string projectAPath = Path.GetRandomFileName();
+ string projectBPath = Path.GetRandomFileName();
+ string projectCPath = Path.GetRandomFileName();
+
SlnProject[] projects =
{
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectA",
+ FullPath = Path.Combine(TestRootPath, projectAPath),
+ Name = Path.GetFileNameWithoutExtension(projectAPath),
ProjectGuid = new Guid("C95D800E-F016-4167-8E1B-1D3FF94CE2E2"),
ProjectTypeGuid = new Guid("88152E7E-47E3-45C8-B5D3-DDB15B2F0435"),
IsMainProject = true,
},
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectB",
+ FullPath = Path.Combine(TestRootPath, projectBPath),
+ Name = Path.GetFileNameWithoutExtension(projectBPath),
ProjectGuid = new Guid("F3CEBCAB-98E5-4041-84DB-033C9682F340"),
ProjectTypeGuid = new Guid("EEC9AD2B-9B7E-4581-864E-76A2BB607C3F"),
IsMainProject = true,
},
new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "ProjectC",
+ FullPath = Path.Combine(TestRootPath, projectCPath),
+ Name = Path.GetFileNameWithoutExtension(projectCPath),
ProjectGuid = new Guid("0079D674-EC4D-4D09-9C4E-699D0D1B0F72"),
ProjectTypeGuid = new Guid("7717E4E9-5443-401B-A964-55727AF96E0C"),
IsMainProject = true,
@@ -1032,12 +1062,13 @@ public void WithFoldersDoNotIgnoreMainProject()
string[] solutionItems = new[] { Path.Combine(root, "SubFolder3", solutionItem1Name) };
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile();
slnFile.AddProjects(projects, new Dictionary(), projects[1].FullPath);
slnFile.AddSolutionItems(solutionItems);
- slnFile.Save(solutionFilePath, true);
+ slnFile.Save(serializer, solutionFilePath, true);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -1090,12 +1121,13 @@ public void WithFoldersIgnoreMainProject()
string[] solutionItems = new[] { Path.Combine(root, "SubFolder3", solutionItem1Name) };
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile();
slnFile.AddProjects(projects, new Dictionary());
slnFile.AddSolutionItems(solutionItems);
- slnFile.Save(solutionFilePath, true);
+ slnFile.Save(serializer, solutionFilePath, true);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -1157,12 +1189,13 @@ public void WithFoldersDoesNotCreateRootFolder(bool ignoreMainProject, bool coll
string[] solutionItems = new[] { Path.Combine(root, "SubFolder3", solutionItem1Name) };
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile();
slnFile.AddProjects(projects, new Dictionary(), ignoreMainProject ? null : projects[1].FullPath);
slnFile.AddSolutionItems(solutionItems);
- slnFile.Save(solutionFilePath, useFolders: true, collapseFolders: collapseFolders);
+ slnFile.Save(serializer, solutionFilePath, useFolders: true, collapseFolders: collapseFolders);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -1211,6 +1244,7 @@ public void WithFoldersDoesNotCreateRootFolder(bool ignoreMainProject, bool coll
public void VisualStudioVersionIsWritten()
{
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile
{
@@ -1218,18 +1252,15 @@ public void VisualStudioVersionIsWritten()
SolutionGuid = new Guid("{6370DE27-36B7-44AE-B47A-1ECF4A6D740A}"),
};
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
File.ReadAllText(solutionFilePath).ShouldBe(
- @"Microsoft Visual Studio Solution File, Format Version 12.00
+ @"
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 1
VisualStudioVersion = 1.2.3.4
MinimumVisualStudioVersion = 10.0.40219.1
Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
@@ -1246,6 +1277,7 @@ public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpec
{
// Arrange
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
var slnFile = new SlnFile()
{
@@ -1255,21 +1287,16 @@ public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpec
slnFile.AddSolutionItems("docs", new[] { Path.Combine(this.TestRootPath, "README.md") });
// Act
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
- // Assert
- File.ReadAllText(solutionFilePath).ShouldBe(
- @"Microsoft Visual Studio Solution File, Format Version 12.00
-Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""docs"", ""docs"", ""{B283EBC2-E01F-412D-9339-FD56EF114549}""
+ const string ExpectedSolutionContents = @"
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""docs"", ""docs"", ""{B283EBC2-E01F-412D-9339-FD56EF114549}""
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
@@ -1277,8 +1304,10 @@ public void Save_WithSolutionItemsAddedToSpecificFolder_SolutionItemsExistInSpec
SolutionGuid = {6370DE27-36B7-44AE-B47A-1ECF4A6D740A}
EndGlobalSection
EndGlobal
-",
- StringCompareShould.IgnoreLineEndings);
+";
+
+ // Assert
+ File.ReadAllText(solutionFilePath).ShouldBe(ExpectedSolutionContents, StringCompareShould.IgnoreLineEndings);
}
[Fact]
@@ -1305,10 +1334,10 @@ public void EmitWindowsWarningForProjectsOnMultipleDrives()
SlnFile slnFile = new ();
SlnProject[] projects = new[] { projectA, projectB };
string solutionFilePath = isWindowsPlatform ? @$"X:\{Path.GetRandomFileName()}" : $"/mnt/{Path.GetRandomFileName()}";
- StringBuilderTextWriter writer = new (new StringBuilder(), new List());
+ ISolutionSerializer serializer = new MockSolutionSerializer();
slnFile.AddProjects(projects);
- slnFile.Save(solutionFilePath, writer, useFolders: true, logger);
+ slnFile.Save(serializer, solutionFilePath, useFolders: true, logger);
logger.Errors.Count.ShouldBe(0);
@@ -1328,7 +1357,6 @@ public void DoNotEmitWarningForRootPath()
{
TestLogger logger = new ();
SlnFile slnFile = new ();
- StringBuilderTextWriter writer = new (new StringBuilder(), new List());
SlnProject project = new SlnProject
{
@@ -1341,8 +1369,10 @@ public void DoNotEmitWarningForRootPath()
};
string solutionFilePath = Path.Combine(TestRootPath, "sample.sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+
slnFile.AddProjects([project]);
- slnFile.Save(solutionFilePath, writer, useFolders: true, logger, collapseFolders: true);
+ slnFile.Save(serializer, solutionFilePath, useFolders: true, logger, collapseFolders: true);
logger.Errors.Count.ShouldBe(0);
logger.Warnings.Count.ShouldBe(0);
@@ -1353,6 +1383,7 @@ public void Save_WithSolutionItemsAddedWithParentFolder_SolutionItemsNestedInPar
{
// Arrange
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
var slnFile = new SlnFile()
{
@@ -1380,26 +1411,24 @@ public void Save_WithSolutionItemsAddedWithParentFolder_SolutionItemsNestedInPar
slnFile.AddProjects(new[] { project });
// Act
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
// Assert
File.ReadAllText(solutionFilePath).ShouldBe(
- @"Microsoft Visual Studio Solution File, Format Version 12.00
+ @"
+Microsoft Visual Studio Solution File, Format Version 12.00
Project(""{65815BD7-8B14-4E69-8328-D5C4ED3245BE}"") = ""ProjectA"", ""ProjectA.csproj"", ""{2ACFA184-2D17-4F80-A132-EC462B48A065}""
EndProject
-Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""docs"", ""docs"", ""{24073434-9641-4234-A3E8-352E5E549B65}""
+Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""docs"", ""docs"", ""{24073434-9641-4234-A3E8-352E5E549B65}""
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
-Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""license"", ""license"", ""{9124D1F8-9153-40CC-BC94-3B2A3AA51E91}""
+Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""license"", ""license"", ""{9124D1F8-9153-40CC-BC94-3B2A3AA51E91}""
ProjectSection(SolutionItems) = preProject
LICENSE.txt = LICENSE.txt
EndProjectSection
EndProject
- GlobalSection(NestedProjects) = preSolution
- {9124D1F8-9153-40CC-BC94-3B2A3AA51E91} = {24073434-9641-4234-A3E8-352E5E549B65}
- EndGlobalSection
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1414,6 +1443,9 @@ public void Save_WithSolutionItemsAddedWithParentFolder_SolutionItemsNestedInPar
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {9124D1F8-9153-40CC-BC94-3B2A3AA51E91} = {24073434-9641-4234-A3E8-352E5E549B65}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6370DE27-36B7-44AE-B47A-1ECF4A6D740A}
EndGlobalSection
@@ -1428,12 +1460,15 @@ public void Save_WithSolutionItemsAddedWithParentFolder_SolutionItemsNestedInPar
public void SlnProject_IsBuildable_ReflectedAsProjectConfigurationInSolutionIncludeInBuild(bool isBuildable)
{
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+
+ string projectFilePath = Path.GetRandomFileName();
SlnFile slnFile = new SlnFile();
SlnProject slnProject = new SlnProject
{
- FullPath = GetTempFileName(),
- Name = "Project",
+ FullPath = Path.Combine(TestRootPath, projectFilePath),
+ Name = Path.GetFileNameWithoutExtension(projectFilePath),
ProjectGuid = Guid.NewGuid(),
ProjectTypeGuid = Guid.NewGuid(),
Configurations = new[] { "Debug", "Release" },
@@ -1442,7 +1477,7 @@ public void SlnProject_IsBuildable_ReflectedAsProjectConfigurationInSolutionIncl
};
slnFile.AddProjects(new[] { slnProject });
- slnFile.Save(solutionFilePath, useFolders: false);
+ slnFile.Save(serializer, solutionFilePath, useFolders: false);
ValidateProjectInSolution(
(slnProject, projectInSolution) =>
@@ -1475,11 +1510,13 @@ private string GetSolutionFilePath(Project[] projects)
private void ValidateProjectInSolution(Action customValidator, SlnProject[] projects, bool useFolders)
{
string solutionFilePath = GetTempFileName(".sln");
+ ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
SlnFile slnFile = new SlnFile();
slnFile.AddProjects(projects);
- slnFile.Save(solutionFilePath, useFolders);
+ slnFile.CreateSolutionDirectory(solutionFilePath);
+ slnFile.Save(serializer, solutionFilePath, useFolders);
SolutionFile solutionFile = SolutionFile.Parse(solutionFilePath);
@@ -1560,5 +1597,30 @@ private ProjectInSolution GetSolutionFolderByName(SolutionFile solutionFile, str
return solutionFile.ProjectsInOrder.FirstOrDefault(i => i.ProjectName.Equals(name));
#endif
}
+
+ private class MockSolutionSerializer : ISolutionSerializer
+ {
+ public string Name => throw new NotImplementedException();
+
+ public ISerializerModelExtension CreateModelExtension()
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsSupported(string moniker)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task OpenAsync(string moniker, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SaveAsync(string moniker, SolutionModel model, CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.VisualStudio.SlnGen/Microsoft.VisualStudio.SlnGen.csproj b/src/Microsoft.VisualStudio.SlnGen/Microsoft.VisualStudio.SlnGen.csproj
index 246f83b..68c9049 100644
--- a/src/Microsoft.VisualStudio.SlnGen/Microsoft.VisualStudio.SlnGen.csproj
+++ b/src/Microsoft.VisualStudio.SlnGen/Microsoft.VisualStudio.SlnGen.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs b/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs
index cb3fe98..1ade71a 100644
--- a/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs
+++ b/src/Microsoft.VisualStudio.SlnGen/SlnFile.cs
@@ -3,13 +3,17 @@
// Licensed under the MIT license.
using Microsoft.Build.Evaluation;
+using Microsoft.VisualStudio.SolutionPersistence;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer.SlnV12;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
-using System.Text.RegularExpressions;
+using System.Threading;
namespace Microsoft.VisualStudio.SlnGen
{
@@ -18,50 +22,7 @@ namespace Microsoft.VisualStudio.SlnGen
///
public sealed class SlnFile
{
- ///
- /// The beginning of the line that ends a global section.
- ///
- private const string GlobalSectionEnd = "\tEndGlobalSection";
-
- ///
- /// The beginning of the line that starts the extensibility global section.
- ///
- private const string GlobalSectionStartExtensibilityGlobals = "\tGlobalSection(ExtensibilityGlobals)";
-
- ///
- /// The solution header.
- ///
- private const string Header = "Microsoft Visual Studio Solution File, Format Version {0}";
-
- ///
- /// The beginning of the line that ends project information.
- ///
- private const string ProjectSectionEnd = "EndProject";
-
- ///
- /// The beginning of the line that contains project information.
- ///
- private const string ProjectSectionStart = "Project(\"";
-
- ///
- /// The beginning of the line that contains the solution GUID.
- ///
- private const string SectionSettingSolutionGuid = "\t\tSolutionGuid = ";
-
- ///
- /// A regular expression used to parse the project section.
- ///
- private static readonly Regex GuidRegex = new (@"(?\{[0-9a-fA-F\-]+\})");
-
- ///
- /// The separator to split project information by.
- ///
- private static readonly string[] ProjectSectionSeparator = { "\", \"" };
-
- ///
- /// The file format version.
- ///
- private readonly string _fileFormatVersion;
+ private static readonly char[] DirectorySeparatorCharacters = new char[] { Path.DirectorySeparatorChar };
///
/// Gets the projects.
@@ -73,20 +34,10 @@ public sealed class SlnFile
///
private readonly Dictionary _solutionItems = new ();
- ///
- /// Initializes a new instance of the class.
- ///
- /// The file format version.
- public SlnFile(string fileFormatVersion)
- {
- _fileFormatVersion = fileFormatVersion;
- }
-
///
/// Initializes a new instance of the class.
///
public SlnFile()
- : this("12.00")
{
}
@@ -156,6 +107,7 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int
}
var firstProjectName = firstProject.GetPropertyValueOrDefault(MSBuildPropertyNames.SlnGenProjectName, Path.GetFileName(firstProject.FullPath));
+
string solutionFileName = Path.ChangeExtension(firstProjectName, "sln");
solutionFileFullPath = Path.Combine(solutionDirectoryFullPath!, solutionFileName);
@@ -172,7 +124,7 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int
}
}
- SlnFile solution = new SlnFile
+ SlnFile slnFile = new ()
{
Platforms = arguments.GetPlatforms(),
Configurations = arguments.GetConfigurations(),
@@ -182,10 +134,10 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int
{
if (arguments.VisualStudioVersion.Version != null && Version.TryParse(arguments.VisualStudioVersion.Version, out Version version))
{
- solution.VisualStudioVersion = version;
+ slnFile.VisualStudioVersion = version;
}
- if (solution.VisualStudioVersion == null)
+ if (slnFile.VisualStudioVersion == null)
{
string devEnvFullPath = arguments.GetDevEnvFullPath(Program.CurrentDevelopmentEnvironment.VisualStudio);
@@ -193,18 +145,16 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int
{
FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(devEnvFullPath);
- solution.VisualStudioVersion = new Version(fileVersionInfo.ProductMajorPart, fileVersionInfo.ProductMinorPart, fileVersionInfo.ProductBuildPart, fileVersionInfo.FilePrivatePart);
+ slnFile.VisualStudioVersion = new Version(fileVersionInfo.ProductMajorPart, fileVersionInfo.ProductMinorPart, fileVersionInfo.ProductBuildPart, fileVersionInfo.FilePrivatePart);
}
}
}
- if (TryParseExistingSolution(solutionFileFullPath, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath))
+ if (TryParseExistingSolution(solutionFileFullPath, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath, out ISolutionSerializer serializer))
{
logger.LogMessageNormal("Updating existing solution file and reusing Visual Studio cache");
-
- solution.SolutionGuid = solutionGuid;
- solution.ExistingProjectGuids = projectGuidsByPath;
-
+ slnFile.SolutionGuid = solutionGuid;
+ slnFile.ExistingProjectGuids = projectGuidsByPath;
arguments.LoadProjectsInVisualStudio = new[] { bool.TrueString };
}
@@ -214,112 +164,79 @@ public static (string solutionFileFullPath, int customProjectTypeGuidCount, int
isBuildable = bool.TrueString.Equals(isBuildableString, StringComparison.OrdinalIgnoreCase);
}
- solution.AddProjects(projectList, customProjectTypeGuids, arguments.IgnoreMainProject ? null : firstProject.FullPath, isBuildable);
+ slnFile.AddProjects(projectList, customProjectTypeGuids, arguments.IgnoreMainProject ? null : firstProject.FullPath, isBuildable);
- solution.AddSolutionItems(solutionItems);
+ slnFile.AddSolutionItems(solutionItems);
string slnGenFoldersPropertyValue = firstProject.GetPropertyValueOrDefault(MSBuildPropertyNames.SlnGenFolders, "false");
var enableFolders = arguments.EnableFolders(slnGenFoldersPropertyValue);
if (!logger.HasLoggedErrors)
{
- solution.Save(solutionFileFullPath, enableFolders, logger, arguments.EnableCollapseFolders(), arguments.EnableAlwaysBuild());
+ slnFile.CreateSolutionDirectory(solutionFileFullPath);
+ slnFile.Save(serializer, solutionFileFullPath, enableFolders, logger, arguments.EnableCollapseFolders(), arguments.EnableAlwaysBuild());
}
- return (solutionFileFullPath, customProjectTypeGuids.Count, solutionItems.Count, solution.SolutionGuid);
+ return (solutionFileFullPath, customProjectTypeGuids.Count, solutionItems.Count, solutionGuid);
}
///
/// Attempts to read the existing GUID from a solution file if one exists.
///
- /// The path to a solution file.
+ /// Path to the existing solution file.
/// Receives the of the existing solution file if one is found, otherwise default(Guid).
/// Receives the project GUIDs by their full paths.
- /// true if the solution GUID was found, otherwise false.
- public static bool TryParseExistingSolution(string path, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath)
+ /// Serializer which can reads and determines the appropriate solution model from the solution file (based on the moniker).
+ /// true if the solution file could be correctly parsed.
+ public static bool TryParseExistingSolution(string solutionFileFullPath, out Guid solutionGuid, out IReadOnlyDictionary projectGuidsByPath, out ISolutionSerializer serializer)
{
- solutionGuid = default;
projectGuidsByPath = default;
+ solutionGuid = default;
+ serializer = SolutionSerializers.GetSerializerByMoniker(solutionFileFullPath);
- bool foundSolutionGuid = false;
-
- Dictionary projectGuids = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- FileInfo fileInfo = new FileInfo(path);
-
+ FileInfo fileInfo = new FileInfo(solutionFileFullPath);
if (!fileInfo.Exists || fileInfo.Directory == null)
{
return false;
}
- using FileStream stream = File.OpenRead(path);
- using StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(0), detectEncodingFromByteOrderMarks: true);
-
- string line;
-
- while ((line = reader.ReadLine()) != null)
+ if (serializer is null)
{
- if (line.StartsWith(ProjectSectionStart))
- {
- string[] projectDetails = line.Split(ProjectSectionSeparator, StringSplitOptions.RemoveEmptyEntries);
-
- if (projectDetails.Length == 3)
- {
- Match projectGuidMatch = GuidRegex.Match(projectDetails[2]);
-
- if (!projectGuidMatch.Groups["Guid"].Success)
- {
- continue;
- }
-
- string projectGuidString = projectGuidMatch.Groups["Guid"].Value;
-
- Match projectTypeGuidMatch = GuidRegex.Match(projectDetails[0]);
-
- if (!projectTypeGuidMatch.Groups["Guid"].Success)
- {
- continue;
- }
-
- if (!Guid.TryParse(projectGuidString, out Guid projectGuid) || !Guid.TryParse(projectTypeGuidMatch.Groups["Guid"].Value, out Guid projectTypeGuid))
- {
- continue;
- }
+ return false;
+ }
- string projectPath = projectDetails[1].Trim().Trim('\"');
+ bool foundSolutionGuid = false;
- projectGuids[projectPath] = projectGuid;
- }
+ try
+ {
+ SolutionModel existingSolution = serializer.OpenAsync(solutionFileFullPath, CancellationToken.None).Result;
- while ((line = reader.ReadLine()) != null)
- {
- if (line.StartsWith(ProjectSectionEnd))
- {
- break;
- }
- }
+ Dictionary projectGuids = new (StringComparer.OrdinalIgnoreCase);
+ foreach (SolutionProjectModel project in existingSolution.SolutionProjects)
+ {
+ projectGuids[project.FilePath] = project.Id;
}
- if (line != null && line.StartsWith(GlobalSectionStartExtensibilityGlobals))
+ foreach (SolutionFolderModel folder in existingSolution.SolutionFolders)
{
- while ((line = reader.ReadLine()) != null)
- {
- if (line.StartsWith(SectionSettingSolutionGuid))
- {
- string solutionGuidString = line.Substring(SectionSettingSolutionGuid.Length);
-
- foundSolutionGuid = Guid.TryParse(solutionGuidString, out solutionGuid);
- }
+ projectGuids[GetSolutionFolderPathWithForwardSlashes(folder.Path)] = folder.Id;
+ }
- if (line.StartsWith(GlobalSectionEnd))
- {
- break;
- }
- }
+ IEnumerable existingSlnProperties = existingSolution.GetSlnProperties();
+ SolutionPropertyBag extensibilityGlobals = existingSlnProperties.Where(x => x.Id == "ExtensibilityGlobals").FirstOrDefault();
+ if (extensibilityGlobals is not null)
+ {
+ extensibilityGlobals.TryGetValue("SolutionGuid", out string solutionGuidStr);
+ foundSolutionGuid = Guid.TryParse(solutionGuidStr, out solutionGuid);
}
- }
- projectGuidsByPath = projectGuids;
+ projectGuidsByPath = projectGuids;
+ }
+ catch (SolutionException)
+ {
+ // There was an unrecoverable syntax error reading the solution file.
+ return false;
+ }
return foundSolutionGuid;
}
@@ -392,47 +309,49 @@ public void AddSolutionItems(Guid? parentFolderGuid, string folderPath, Guid fol
}
///
- /// Saves the Visual Studio solution to a file.
+ /// Creates the directory where the solution resides.
///
- /// The full path to the file to write to.
- /// Specifies if folders should be created.
- /// A to use for logging.
- /// An optional value indicating whether or not folders containing a single item should be collapsed into their parent folder.
- /// An optional value indicating whether or not to always include the project in the build even if it has no matching configuration.
- public void Save(string path, bool useFolders, ISlnGenLogger logger = null, bool collapseFolders = false, bool alwaysBuild = true)
+ /// A root path for the solution.
+ internal void CreateSolutionDirectory(string rootPath)
{
- string directoryName = Path.GetDirectoryName(path);
+ string directoryName = Path.GetDirectoryName(rootPath);
if (!directoryName.IsNullOrWhiteSpace())
{
Directory.CreateDirectory(directoryName!);
}
-
- using FileStream fileStream = File.Create(path);
-
- using StreamWriter writer = new StreamWriter(fileStream, Encoding.UTF8);
-
- Save(path, writer, useFolders, logger, collapseFolders, alwaysBuild);
}
///
/// Saves the Visual Studio solution to a file.
///
+ /// Serializer which saves the solution model to the solution file (based on its moniker).
/// A root path for the solution to make other paths relative to.
- /// The to save the solution file to.
/// Specifies if folders should be created.
/// A to use for logging.
/// An optional value indicating whether or not folders containing a single item should be collapsed into their parent folder.
/// An optional value indicating whether or not to always include the project in the build even if it has no matching configuration.
- internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenLogger logger = null, bool collapseFolders = false, bool alwaysBuild = true)
+ internal void Save(ISolutionSerializer serializer, string rootPath, bool useFolders, ISlnGenLogger logger = null, bool collapseFolders = false, bool alwaysBuild = true)
{
- writer.WriteLine(Header, _fileFormatVersion);
+ SolutionModel newSolution = new ();
+
+ // Set UTF8 BOM encoding for .sln
+ if (serializer is ISolutionSerializer v12Serializer)
+ {
+ newSolution.SerializerExtension = v12Serializer.CreateModelExtension(new ()
+ {
+ Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true),
+ });
+ }
if (VisualStudioVersion != null)
{
- writer.WriteLine($"# Visual Studio Version {VisualStudioVersion.Major}");
- writer.WriteLine($"VisualStudioVersion = {VisualStudioVersion}");
- writer.WriteLine($"MinimumVisualStudioVersion = {MinimumVisualStudioVersion}");
+ newSolution.VisualStudioProperties.OpenWith = $"Visual Studio Version {VisualStudioVersion.Major}";
+ newSolution.VisualStudioProperties.Version = VisualStudioVersion;
+ if (Version.TryParse(MinimumVisualStudioVersion, out var minimumVisualStudioVersion))
+ {
+ newSolution.VisualStudioProperties.MinimumVersion = minimumVisualStudioVersion;
+ }
}
List sortedProjects = _projects.OrderBy(i => i.IsMainProject ? 0 : 1).ThenBy(i => i.FullPath).ToList();
@@ -445,8 +364,9 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
project.ProjectGuid = existingProjectGuid;
}
- writer.WriteLine($@"Project(""{project.ProjectTypeGuid.ToSolutionString()}"") = ""{project.Name}"", ""{solutionPath}"", ""{project.ProjectGuid.ToSolutionString()}""");
- writer.WriteLine("EndProject");
+ SolutionProjectModel projectModel = newSolution.AddProject(solutionPath, project.ProjectTypeGuid.ToSolutionString(), null);
+ projectModel.DisplayName = project.Name;
+ projectModel.Id = project.ProjectGuid;
}
SlnHierarchy hierarchy = null;
@@ -466,9 +386,8 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
{
if (solutionItems.Value.SolutionItems.Any())
{
- writer.WriteLine($@"Project(""{SlnFolder.FolderProjectTypeGuidString}"") = ""{solutionItems.Key}"", ""{solutionItems.Key}"", ""{solutionItems.Value.FolderGuid.ToSolutionString()}"" ");
- WriteSolutionItemsProjectSection(rootPath, writer, solutionItems.Value.SolutionItems);
- writer.WriteLine("EndProject");
+ SolutionFolderModel newFolder = AddFolderToModel(newSolution, solutionItems.Key, solutionItems.Value.FolderGuid);
+ AddSolutionItemsToModel(newFolder, solutionItems.Value.SolutionItems, rootPath);
}
}
@@ -476,14 +395,7 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
var solutionItemsWithParents = _solutionItems.Where(x => x.Value.ParentFolderGuid.HasValue).ToArray();
if (solutionItemsWithParents.Length > 0)
{
- writer.WriteLine(@" GlobalSection(NestedProjects) = preSolution");
-
- foreach (KeyValuePair solutionItem in solutionItemsWithParents)
- {
- writer.WriteLine($@" {solutionItem.Value.FolderGuid.ToSolutionString()} = {solutionItem.Value.ParentFolderGuid.Value.ToSolutionString()}");
- }
-
- writer.WriteLine(" EndGlobalSection");
+ AddNestedProjectsToModel(newSolution, solutionItemsWithParents);
}
}
@@ -523,27 +435,22 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
// guard against root folder
if (folder != hierarchy.RootFolder)
{
- writer.WriteLine($@"Project(""{folder.ProjectTypeGuidString}"") = ""{folder.Name}"", ""{projectSolutionPath}"", ""{folder.FolderGuid.ToSolutionString()}""");
+ SolutionFolderModel newFolder = AddFolderToModel(newSolution, folder.Name, folder.FolderGuid);
if (folder.SolutionItems.Count > 0)
{
- WriteSolutionItemsProjectSection(rootPath, writer, folder.SolutionItems);
+ AddSolutionItemsToModel(newFolder, folder.SolutionItems, rootPath);
}
-
- writer.WriteLine("EndProject");
}
else if (folder.SolutionItems.Count > 0)
{
// Special case for solution items in root folder
- writer.WriteLine($@"Project(""{SlnFolder.FolderProjectTypeGuidString}"") = ""Solution Items"", ""Solution Items"", ""{{B283EBC2-E01F-412D-9339-FD56EF114549}}"" ");
- WriteSolutionItemsProjectSection(rootPath, writer, folder.SolutionItems);
- writer.WriteLine("EndProject");
+ SolutionFolderModel newFolder = AddFolderToModel(newSolution, "Solution Items", new Guid("B283EBC2-E01F-412D-9339-FD56EF114549"));
+ AddSolutionItemsToModel(newFolder, folder.SolutionItems, rootPath);
}
}
- }
- writer.WriteLine("Global");
-
- writer.WriteLine(" GlobalSection(SolutionConfigurationPlatforms) = preSolution");
+ AddHierarchyNestedProjectsToModel(newSolution, hierarchy);
+ }
HashSet solutionPlatforms = Platforms != null && Platforms.Any()
? new HashSet(GetValidSolutionPlatforms(Platforms), StringComparer.OrdinalIgnoreCase)
@@ -553,23 +460,104 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
? new HashSet(Configurations, StringComparer.OrdinalIgnoreCase)
: new HashSet(sortedProjects.SelectMany(i => i.Configurations).Where(i => !i.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase);
- foreach (string configuration in solutionConfigurations)
+ AddSolutionConfigurationPlatformsToModel(newSolution, solutionConfigurations, solutionPlatforms);
+
+ bool hasSharedProject = AddProjectConfigurationPlatformsToModel(newSolution, sortedProjects, solutionConfigurations, solutionPlatforms, alwaysBuild);
+
+ if (hasSharedProject)
{
- foreach (string platform in solutionPlatforms)
+ AddSharedMSBuildProjectFilesToModel(newSolution, sortedProjects, rootPath);
+ }
+
+ AddSolutionGuidToModel(newSolution);
+
+ serializer.SaveAsync(rootPath, newSolution, CancellationToken.None).Wait();
+ }
+
+ private static string GetSolutionFolderPathWithForwardSlashes(string path)
+ {
+ // SolutionModel::AddFolder expects paths to have leading, trailing and inner forward slashes
+ // https://github.com/microsoft/vs-solutionpersistence/blob/87ee8ea069662d55c336a9bd68fe4851d0384fa5/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs#L171C1-L172C1
+ return "/" + string.Join("/", GetPathWithDirectorySeparator(path).Split(DirectorySeparatorCharacters, StringSplitOptions.RemoveEmptyEntries)) + "/";
+ }
+
+ private static string GetPathWithDirectorySeparator(string path) => path.Replace('\\', '/');
+
+ private static SolutionFolderModel AddFolderToModel(SolutionModel newSolution, string solutionFolder, Guid folderGuid)
+ {
+ SolutionFolderModel solutionFolderModel = newSolution.AddFolder(GetSolutionFolderPathWithForwardSlashes(solutionFolder));
+ solutionFolderModel.Id = folderGuid;
+ return solutionFolderModel;
+ }
+
+ private static void AddSolutionItemsToModel(SolutionFolderModel newFolder, IEnumerable solutionItems, string rootPath)
+ {
+ SolutionPropertyBag slnProperties = new ("SolutionItems", scope: PropertiesScope.PreLoad);
+ foreach (string solutionItem in solutionItems
+ .Select(i => i.ToRelativePath(rootPath).ToSolutionPath())
+ .Where(i => !string.IsNullOrWhiteSpace(i)))
+ {
+ slnProperties.Add(solutionItem, solutionItem);
+ }
+
+ newFolder.AddSlnProperties(slnProperties);
+ }
+
+ private static void AddNestedProjectsToModel(SolutionModel newSolution, KeyValuePair[] solutionItemsWithParents)
+ {
+ SolutionPropertyBag slnProperties = new ("NestedProjects", scope: PropertiesScope.PreLoad);
+ foreach (KeyValuePair solutionItem in solutionItemsWithParents)
+ {
+ slnProperties.Add(solutionItem.Value.FolderGuid.ToSolutionString(), solutionItem.Value.ParentFolderGuid.Value.ToSolutionString());
+ }
+
+ newSolution.AddSlnProperties(slnProperties);
+ }
+
+ private static void AddHierarchyNestedProjectsToModel(SolutionModel newSolution, SlnHierarchy hierarchy)
+ {
+ var foldersWithParents = hierarchy.Folders.Where(i => i.Parent != null).ToArray();
+ if (foldersWithParents.Length > 0)
+ {
+ SolutionPropertyBag slnProperties = new ("NestedProjects", scope: PropertiesScope.PreLoad);
+ foreach (SlnFolder folder in foldersWithParents)
{
- if (!string.IsNullOrWhiteSpace(configuration) && !string.IsNullOrWhiteSpace(platform))
+ foreach (SlnProject project in folder.Projects)
+ {
+ slnProperties.Add(project.ProjectGuid.ToSolutionString(), folder.FolderGuid.ToSolutionString());
+ }
+
+ // guard against root folder
+ if (folder.Parent != hierarchy.RootFolder)
{
- writer.WriteLine($" {configuration}|{platform} = {configuration}|{platform}");
+ slnProperties.Add(folder.FolderGuid.ToSolutionString(), folder.Parent.FolderGuid.ToSolutionString());
}
}
+
+ newSolution.AddSlnProperties(slnProperties);
}
+ }
- writer.WriteLine(" EndGlobalSection");
+ private void AddSharedMSBuildProjectFilesToModel(SolutionModel newSolution, List sortedProjects, string rootPath)
+ {
+ SolutionPropertyBag slnProperties = new ("SharedMSBuildProjectFiles", scope: PropertiesScope.PreLoad);
+ foreach (SlnProject project in sortedProjects)
+ {
+ foreach (string sharedProjectItem in project.SharedProjectItems)
+ {
+ slnProperties.Add($"{sharedProjectItem.ToRelativePath(rootPath).ToSolutionPath()}*{project.ProjectGuid.ToSolutionString(uppercase: false).ToLowerInvariant()}*SharedItemsImports", $"{GetSharedProjectOptions(project)}");
+ }
+ }
- writer.WriteLine(" GlobalSection(ProjectConfigurationPlatforms) = postSolution");
+ newSolution.AddSlnProperties(slnProperties);
+ }
+ private bool AddProjectConfigurationPlatformsToModel(SolutionModel newSolution, List sortedProjects, HashSet solutionConfigurations, HashSet solutionPlatforms, bool alwaysBuild)
+ {
bool hasSharedProject = false;
+ SolutionPropertyBag slnProperties = new ("ProjectConfigurationPlatforms", scope: PropertiesScope.PostLoad);
+
foreach (SlnProject project in sortedProjects)
{
if (project.IsSharedProject)
@@ -588,88 +576,51 @@ internal void Save(string rootPath, TextWriter writer, bool useFolders, ISlnGenL
{
bool foundPlatform = TryGetProjectSolutionPlatform(platform, project, out string projectSolutionPlatform, out string projectBuildPlatform);
- writer.WriteLine($@" {projectGuid}.{configuration}|{platform}.ActiveCfg = {projectSolutionConfiguration}|{projectSolutionPlatform}");
+ slnProperties.Add($"{projectGuid}.{configuration}|{platform}.ActiveCfg", $"{projectSolutionConfiguration}|{projectSolutionPlatform}");
if (foundPlatform && foundConfiguration && project.IsBuildable)
{
- writer.WriteLine($@" {projectGuid}.{configuration}|{platform}.Build.0 = {projectSolutionConfiguration}|{projectBuildPlatform}");
+ slnProperties.Add($"{projectGuid}.{configuration}|{platform}.Build.0", $"{projectSolutionConfiguration}|{projectBuildPlatform}");
}
if (project.IsDeployable)
{
- writer.WriteLine($@" {projectGuid}.{configuration}|{platform}.Deploy.0 = {projectSolutionConfiguration}|{projectSolutionPlatform}");
+ slnProperties.Add($"{projectGuid}.{configuration}|{platform}.Deploy.0", $"{projectSolutionConfiguration}|{projectSolutionPlatform}");
}
}
}
}
- writer.WriteLine(" EndGlobalSection");
-
- writer.WriteLine(" GlobalSection(SolutionProperties) = preSolution");
- writer.WriteLine(" HideSolutionNode = FALSE");
- writer.WriteLine(" EndGlobalSection");
-
- if (hierarchy != null)
- {
- var foldersWithParents = hierarchy.Folders.Where(i => i.Parent != null).ToArray();
- if (foldersWithParents.Length > 0)
- {
- writer.WriteLine(@" GlobalSection(NestedProjects) = preSolution");
-
- foreach (SlnFolder folder in foldersWithParents)
- {
- foreach (SlnProject project in folder.Projects)
- {
- writer.WriteLine($@" {project.ProjectGuid.ToSolutionString()} = {folder.FolderGuid.ToSolutionString()}");
- }
-
- // guard against root folder
- if (folder.Parent != hierarchy.RootFolder)
- {
- writer.WriteLine($@" {folder.FolderGuid.ToSolutionString()} = {folder.Parent.FolderGuid.ToSolutionString()}");
- }
- }
-
- writer.WriteLine(" EndGlobalSection");
- }
- }
+ newSolution.AddSlnProperties(slnProperties);
- writer.WriteLine(" GlobalSection(ExtensibilityGlobals) = postSolution");
- writer.WriteLine($" SolutionGuid = {SolutionGuid.ToSolutionString()}");
- writer.WriteLine(" EndGlobalSection");
+ return hasSharedProject;
+ }
- if (hasSharedProject)
+ private void AddSolutionConfigurationPlatformsToModel(SolutionModel newSolution, HashSet solutionConfigurations, HashSet solutionPlatforms)
+ {
+ SolutionPropertyBag slnProperties = new ("SolutionConfigurationPlatforms", scope: PropertiesScope.PreLoad);
+ foreach (string configuration in solutionConfigurations)
{
- writer.WriteLine(" GlobalSection(SharedMSBuildProjectFiles) = preSolution");
-
- foreach (SlnProject project in sortedProjects)
+ foreach (string platform in solutionPlatforms)
{
- foreach (string sharedProjectItem in project.SharedProjectItems)
+ if (!string.IsNullOrWhiteSpace(configuration) && !string.IsNullOrWhiteSpace(platform))
{
- writer.WriteLine($" {sharedProjectItem.ToRelativePath(rootPath).ToSolutionPath()}*{project.ProjectGuid.ToSolutionString(uppercase: false).ToLowerInvariant()}*SharedItemsImports = {GetSharedProjectOptions(project)}");
+ slnProperties.Add($"{configuration}|{platform}", $"{configuration}|{platform}");
}
}
-
- writer.WriteLine(" EndGlobalSection");
}
- writer.WriteLine("EndGlobal");
+ newSolution.AddSlnProperties(slnProperties);
}
- private static void WriteSolutionItemsProjectSection(
- string rootPath,
- TextWriter writer,
- IEnumerable solutionItems)
+ private void AddSolutionGuidToModel(SolutionModel newSolution)
{
- writer.WriteLine(" ProjectSection(SolutionItems) = preProject");
- foreach (string solutionItem in solutionItems
- .Select(i => i.ToRelativePath(rootPath).ToSolutionPath())
- .Where(i => !string.IsNullOrWhiteSpace(i)))
+ SolutionPropertyBag newExtensibilityGlobals = new ("ExtensibilityGlobals")
{
- writer.WriteLine($" {solutionItem} = {solutionItem}");
- }
+ { "SolutionGuid", SolutionGuid.ToString() },
+ };
- writer.WriteLine(" EndProjectSection");
+ newSolution.AddSlnProperties(newExtensibilityGlobals);
}
private string GetSharedProjectOptions(SlnProject project)