diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
index b1bf4906..a4da969e 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionModel.cs
@@ -88,6 +88,7 @@ public SolutionModel(SolutionModel solutionModel)
}
this.Description = solutionModel.Description;
+ this.SortOrder = solutionModel.SortOrder;
this.solutionBuildTypes = [.. solutionModel.solutionBuildTypes];
this.solutionPlatforms = [.. solutionModel.solutionPlatforms];
this.projectTypes = [.. solutionModel.projectTypes];
@@ -111,6 +112,15 @@ public SolutionModel(SolutionModel solutionModel)
///
public string? Description { get; set; }
+ ///
+ /// Gets or sets the order in which solution items are written to a .slnx file.
+ ///
+ ///
+ /// The default is , which groups items by type and sorts them alphabetically.
+ /// Set to to preserve the order the items appear in the solution file.
+ ///
+ public SolutionSortOrder SortOrder { get; set; }
+
///
/// Gets the list of solution items in the solution.
/// This is all of the solution folders and projects in the solution.
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionSortOrder.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionSortOrder.cs
new file mode 100644
index 00000000..76dfecdb
--- /dev/null
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Model/SolutionSortOrder.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.VisualStudio.SolutionPersistence.Model;
+
+///
+/// Specifies how solution items are ordered when serializing a .slnx file.
+///
+public enum SolutionSortOrder
+{
+ ///
+ /// Items are grouped by type and sorted alphabetically. This is the default.
+ ///
+ Alphabetical = 0,
+
+ ///
+ /// Items are written in document order, i.e. the order the elements appear in the solution file.
+ ///
+ Document = 1,
+}
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt b/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
index d34f9569..2c0f9ea8 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/PublicAPI/PublicAPI.Unshipped.txt
@@ -34,4 +34,9 @@ Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType.UnsupportedVe
Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.ErrorType.get -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType?
Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.ErrorType.init -> void
Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.SolutionException(string! message, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType errorType) -> void
-Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.SolutionException(string! message, System.Exception! inner, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType errorType) -> void
\ No newline at end of file
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException.SolutionException(string! message, System.Exception! inner, Microsoft.VisualStudio.SolutionPersistence.Model.SolutionErrorType errorType) -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionModel.SortOrder.get -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionModel.SortOrder.set -> void
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder.Alphabetical = 0 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder
+Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder.Document = 1 -> Microsoft.VisualStudio.SolutionPersistence.Model.SolutionSortOrder
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Keywords.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Keywords.cs
index 63c95801..f65cd107 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Keywords.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Keywords.cs
@@ -16,6 +16,7 @@ internal enum Keyword
// Solution properties
Description,
Version,
+ Sort,
// Solution sections
Configurations,
@@ -64,6 +65,8 @@ internal static class Keywords
internal const string XmlTrue = "true";
internal const string XmlFalse = "false";
+ internal const string SortDocument = "Document";
+
private static readonly string[] KeywordToString;
private static readonly Lictionary StringToKeyword;
@@ -74,6 +77,7 @@ static Keywords()
new(nameof(Keyword.Solution), Keyword.Solution),
new(nameof(Keyword.Description), Keyword.Description),
new(nameof(Keyword.Version), Keyword.Version),
+ new(nameof(Keyword.Sort), Keyword.Sort),
new(nameof(Keyword.Configurations), Keyword.Configurations),
new(nameof(Keyword.Folder), Keyword.Folder),
new(nameof(Keyword.Project), Keyword.Project),
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Slnx.xsd b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Slnx.xsd
index 399bf6ed..e6caf5f2 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Slnx.xsd
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/Slnx.xsd
@@ -9,6 +9,7 @@
+
@@ -115,4 +116,4 @@
-
\ No newline at end of file
+
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
index 36c75fea..a9456bf3 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/ItemRefList`1.cs
@@ -48,6 +48,8 @@ internal readonly void Add(T item)
internal readonly T? FirstOrDefault() => this.items.Count > 0 ? this.items[0] : null;
+ internal readonly bool TryGet(string itemRef, [NotNullWhen(true)] out T? item) => this.items.TryGetValue(itemRef, out item);
+
// Finds the item that would be immediately after the given item ref.
internal readonly bool TryFindNext(string itemRef, out T? item)
{
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.ApplyModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.ApplyModel.cs
index d5543ae2..fb76f793 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.ApplyModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlContainer.ApplyModel.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Xml;
+using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Utilities;
namespace Microsoft.VisualStudio.SolutionPersistence.Serializer.Xml.XmlDecorators;
@@ -121,11 +122,19 @@ internal bool ApplyModelItemsToXml(
}
// Add new elements that aren't already in the XML.
- modelItems.Sort(decoratorItems.IgnoreCase ? ComparisonOrdinalIgnoreCase : ComparisonOrdinal);
+ bool documentOrder = this.Root.Solution?.SortOrder == SolutionSortOrder.Document;
+ if (!documentOrder)
+ {
+ modelItems.Sort(decoratorItems.IgnoreCase ? ComparisonOrdinalIgnoreCase : ComparisonOrdinal);
+ }
+
foreach ((string itemRef, TModelItem modelItem) in modelItems)
{
// Find position to insert before based on general areas and alphabetical order.
- XmlDecorator? insertBefore = decoratorItems.TryFindNext(itemRef, out TDecorator? insertBeforeLocal) ? insertBeforeLocal : this.FindNextDecorator();
+ // In document order mode new items are appended to the end of their area in model order.
+ XmlDecorator? insertBefore = documentOrder
+ ? this.FindNextDecorator()
+ : (decoratorItems.TryFindNext(itemRef, out TDecorator? insertBeforeLocal) ? insertBeforeLocal : this.FindNextDecorator());
TDecorator newDecorator = (TDecorator)this.CreateAndAddChild(decoratorElementName, itemRef, insertBefore);
_ = applyModelToXml?.Invoke(newDecorator, modelItem);
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlFolder.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlFolder.cs
index dd75d2d1..cbcd4941 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlFolder.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlFolder.cs
@@ -94,9 +94,24 @@ internal void AddToModel(SolutionModel solutionModel, List<(XmlProject XmlProjec
properties.AddToModel(folderModel);
}
- foreach (XmlProject project in this.folderProjects.GetItems())
+ if (this.xmlSolution.SortOrder == SolutionSortOrder.Document)
{
- newProjects.Add((project, project.AddToModel(solutionModel)));
+ // Preserve the order the elements appear in the document.
+ foreach (XmlElement childElement in this.XmlElement.ChildElements())
+ {
+ if (Keywords.ToKeyword(childElement.Name) == Keyword.Project &&
+ this.folderProjects.TryGet(childElement.GetAttribute(Keyword.Path.ToXmlString()).Trim(), out XmlProject? project))
+ {
+ newProjects.Add((project, project.AddToModel(solutionModel)));
+ }
+ }
+ }
+ else
+ {
+ foreach (XmlProject project in this.folderProjects.GetItems())
+ {
+ newProjects.Add((project, project.AddToModel(solutionModel)));
+ }
}
}
catch (Exception ex) when (SolutionException.ShouldWrap(ex))
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.ApplyModel.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.ApplyModel.cs
index 09b857e2..6046958f 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.ApplyModel.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.ApplyModel.cs
@@ -24,6 +24,12 @@ internal bool ApplyModelToXml(SolutionModel modelSolution)
modified = true;
}
+ if (this.SortOrder != modelSolution.SortOrder)
+ {
+ this.SortOrder = modelSolution.SortOrder;
+ modified = true;
+ }
+
// Configurations
// Use the item ref logic to allow only a single "Configurations" element, and use string.Empty as the item ref.
modified |= this.ApplyModelItemsToXml(
diff --git a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
index cb998ddc..e7c22e59 100644
--- a/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
+++ b/src/Microsoft.VisualStudio.SolutionPersistence/Serializer/Xml/XmlDecorators/XmlSolution.cs
@@ -28,6 +28,18 @@ internal string? Version
set => this.UpdateXmlAttribute(Keyword.Version, value);
}
+ internal string? Sort
+ {
+ get => this.GetXmlAttribute(Keyword.Sort);
+ set => this.UpdateXmlAttribute(Keyword.Sort, value);
+ }
+
+ internal SolutionSortOrder SortOrder
+ {
+ get => StringComparer.OrdinalIgnoreCase.Equals(this.Sort, Keywords.SortDocument) ? SolutionSortOrder.Document : SolutionSortOrder.Alphabetical;
+ set => this.Sort = value == SolutionSortOrder.Document ? Keywords.SortDocument : null;
+ }
+
#if DEBUG
internal override string DebugDisplay => $"{base.DebugDisplay} RootProjects={this.rootProjects} Folders={this.folders}";
@@ -109,20 +121,48 @@ internal SolutionModel ToModel()
{
StringTable = this.Root.StringTable,
Description = this.Description,
+ SortOrder = this.SortOrder,
// Project types are loaded earlier when parsing the XML since they are needed to resolve projects.
ProjectTypes = this.Root.ProjectTypes.ProjectTypes,
};
List<(XmlProject, SolutionProjectModel)> newProjects = new List<(XmlProject, SolutionProjectModel)>(this.rootProjects.ItemsCount);
- foreach (XmlProject project in this.rootProjects.GetItems())
+ if (this.SortOrder == SolutionSortOrder.Document)
{
- newProjects.Add((project, project.AddToModel(solutionModel)));
+ // Preserve the order the elements appear in the document.
+ foreach (XmlElement childElement in this.XmlElement.ChildElements())
+ {
+ switch (Keywords.ToKeyword(childElement.Name))
+ {
+ case Keyword.Project:
+ if (this.rootProjects.TryGet(childElement.GetAttribute(Keyword.Path.ToXmlString()).Trim(), out XmlProject? project))
+ {
+ newProjects.Add((project, project.AddToModel(solutionModel)));
+ }
+
+ break;
+ case Keyword.Folder:
+ if (this.folders.TryGet(childElement.GetAttribute(Keyword.Name.ToXmlString()).Trim(), out XmlFolder? folder))
+ {
+ folder.AddToModel(solutionModel, newProjects);
+ }
+
+ break;
+ }
+ }
}
-
- foreach (XmlFolder folder in this.folders.GetItems())
+ else
{
- folder.AddToModel(solutionModel, newProjects);
+ foreach (XmlProject project in this.rootProjects.GetItems())
+ {
+ newProjects.Add((project, project.AddToModel(solutionModel)));
+ }
+
+ foreach (XmlFolder folder in this.folders.GetItems())
+ {
+ folder.AddToModel(solutionModel, newProjects);
+ }
}
// Dependencies need to be added after all the projects are loaded.
diff --git a/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/SortOrder.cs b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/SortOrder.cs
new file mode 100644
index 00000000..fbd3a1f1
--- /dev/null
+++ b/test/Microsoft.VisualStudio.SolutionPersistence.Tests/Serialization/SortOrder.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Serialization;
+
+///
+/// Tests related to .
+///
+public class SortOrder
+{
+ ///
+ /// Ensures is written to the file and round-trips.
+ ///
+ [Fact]
+ public async Task DocumentRoundTripsAsync()
+ {
+ SolutionModel solution = new SolutionModel { SortOrder = SolutionSortOrder.Document };
+ _ = solution.AddProject("A.csproj");
+
+ (SolutionModel reserializedSolution, FileContents contents) = await SaveAndReopenModelAsync(SolutionSerializers.SlnXml, solution);
+
+ Assert.Contains("Sort=\"Document\"", contents.FullString);
+ Assert.Equal(SolutionSortOrder.Document, reserializedSolution.SortOrder);
+ }
+
+ ///
+ /// Ensures is omitted from the file and round-trips.
+ ///
+ [Fact]
+ public async Task AlphabeticalIsOmittedAsync()
+ {
+ SolutionModel solution = new SolutionModel();
+ _ = solution.AddProject("A.csproj");
+
+ (SolutionModel reserializedSolution, FileContents contents) = await SaveAndReopenModelAsync(SolutionSerializers.SlnXml, solution);
+
+ Assert.DoesNotContain("Sort=", contents.FullString);
+ Assert.Equal(SolutionSortOrder.Alphabetical, reserializedSolution.SortOrder);
+ }
+
+ ///
+ /// Ensures an unrecognized Sort value is treated as .
+ ///
+ [Fact]
+ public async Task UnknownSortValueIsAlphabeticalAsync()
+ {
+ const string slnx = """
+
+
+
+""";
+
+ using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(slnx));
+ SolutionModel solution = await SolutionSerializers.SlnXml.OpenAsync(stream, CancellationToken.None);
+
+ Assert.Equal(SolutionSortOrder.Alphabetical, solution.SortOrder);
+ }
+
+ ///
+ /// Ensures the Sort value is parsed case-insensitively.
+ ///
+ [Fact]
+ public async Task SortValueIsCaseInsensitiveAsync()
+ {
+ const string slnx = """
+
+
+
+""";
+
+ using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(slnx));
+ SolutionModel solution = await SolutionSerializers.SlnXml.OpenAsync(stream, CancellationToken.None);
+
+ Assert.Equal(SolutionSortOrder.Document, solution.SortOrder);
+ }
+}