Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -111,6 +112,15 @@ public SolutionModel(SolutionModel solutionModel)
/// </summary>
public string? Description { get; set; }

/// <summary>
/// Gets or sets the order in which solution items are written to a .slnx file.
/// </summary>
/// <remarks>
/// The default is <see cref="SolutionSortOrder.Alphabetical"/>, which groups items by type and sorts them alphabetically.
/// Set to <see cref="SolutionSortOrder.Document"/> to preserve the order the items appear in the solution file.
/// </remarks>
public SolutionSortOrder SortOrder { get; set; }

/// <summary>
/// Gets the list of solution items in the solution.
/// This is all of the solution folders and projects in the solution.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Specifies how solution items are ordered when serializing a .slnx file.
/// </summary>
public enum SolutionSortOrder
{
/// <summary>
/// Items are grouped by type and sorted alphabetically. This is the default.
/// </summary>
Alphabetical = 0,

/// <summary>
/// Items are written in document order, i.e. the order the elements appear in the solution file.
/// </summary>
Document = 1,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal enum Keyword
// Solution properties
Description,
Version,
Sort,

// Solution sections
Configurations,
Expand Down Expand Up @@ -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<string, Keyword> StringToKeyword;

Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</xs:choice>
<xs:attribute name="Description" type="xs:string" />
<xs:attribute name="Version" type="xs:string" />
<xs:attribute name="Sort" type="xs:string" />
</xs:complexType>
</xs:element>

Expand Down Expand Up @@ -115,4 +116,4 @@
</xs:element>
</xs:choice>
</xs:group>
</xs:schema>
</xs:schema>
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,11 +122,19 @@ internal bool ApplyModelItemsToXml<TModelItem, TDecorator>(
}

// 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<TDecorator>();
// In document order mode new items are appended to the end of their area in model order.
XmlDecorator? insertBefore = documentOrder
? this.FindNextDecorator<TDecorator>()
: (decoratorItems.TryFindNext(itemRef, out TDecorator? insertBeforeLocal) ? insertBeforeLocal : this.FindNextDecorator<TDecorator>());

TDecorator newDecorator = (TDecorator)this.CreateAndAddChild(decoratorElementName, itemRef, insertBefore);
_ = applyModelToXml?.Invoke(newDecorator, modelItem);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SolutionModel, XmlConfigurations>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Tests related to <see cref="SolutionSortOrder"/>.
/// </summary>
public class SortOrder
{
/// <summary>
/// Ensures <see cref="SolutionSortOrder.Document"/> is written to the file and round-trips.
/// </summary>
[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);
}

/// <summary>
/// Ensures <see cref="SolutionSortOrder.Alphabetical"/> is omitted from the file and round-trips.
/// </summary>
[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);
}

/// <summary>
/// Ensures an unrecognized Sort value is treated as <see cref="SolutionSortOrder.Alphabetical"/>.
/// </summary>
[Fact]
public async Task UnknownSortValueIsAlphabeticalAsync()
{
const string slnx = """
<Solution Sort="Bogus">
<Project Path="A.csproj" />
</Solution>
""";

using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(slnx));
SolutionModel solution = await SolutionSerializers.SlnXml.OpenAsync(stream, CancellationToken.None);

Assert.Equal(SolutionSortOrder.Alphabetical, solution.SortOrder);
}

/// <summary>
/// Ensures the Sort value is parsed case-insensitively.
/// </summary>
[Fact]
public async Task SortValueIsCaseInsensitiveAsync()
{
const string slnx = """
<Solution Sort="document">
<Project Path="A.csproj" />
</Solution>
""";

using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(slnx));
SolutionModel solution = await SolutionSerializers.SlnXml.OpenAsync(stream, CancellationToken.None);

Assert.Equal(SolutionSortOrder.Document, solution.SortOrder);
}
}