Skip to content

SolutionModel incorrectly allows duplicated projects that differ on directory separator characters #134

@TessenR

Description

@TessenR

Run the following program:

using System.Text;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer;

var solutionModel = new SolutionModel();
solutionModel.AddProject(@"Core\Lib1.csproj");
solutionModel.AddProject(@"Core/Lib1.csproj");

Console.WriteLine("After adding a project:");
Console.WriteLine("Projects: " + string.Join(", ", solutionModel.SolutionProjects.Select(x => x.FilePath)));

using var memoryStream = new MemoryStream();
await SolutionSerializers.SlnXml.SaveAsync(memoryStream, solutionModel, CancellationToken.None);
memoryStream.Position = 0;

solutionModel = await SolutionSerializers.SlnXml.OpenAsync(memoryStream, CancellationToken.None);

Console.WriteLine("After saving and re-opening the solution:");
Console.WriteLine("Projects: " + string.Join(", ", solutionModel.SolutionProjects.Select(x => x.FilePath)));

Actual result:

After adding a project:
Projects: Core\Lib1.csproj, Core/Lib1.csproj
Unhandled exception. Microsoft.VisualStudio.SolutionPersistence.Model.SolutionException: Duplicate item 'Core/Lib1.csproj' of type 'Project'. (Parameter 'value')

You can add projects that share the same path that only differ on directory separator characters and solution model will see them as different projects.
However, the model just adds two projects that are effectively the same until you attempt to serialize and deserialize it back - then it normalizes the paths and finally notices the duplication

Expected result:
Either, the model rejects project paths with \ like it does for solution folders,
or the model automatically normalizes the separators

Notes:
The current behavior basically makes it users' responsibility to conform to the undocumented standard path format - I've only found this documentation on an internal static method that acknowledges that the model expects / directory separators.

/// <summary>
/// Converts a serialized path that uses backslashes to a model path that uses the platform's directory separator.
/// This is used by the .sln serializer.
/// </summary>
[return: NotNullIfNotNull(nameof(persistencePath))]
internal static string? ConvertBackslashToModel(string? persistencePath)
{
return persistencePath.IsNullOrEmpty() || IsWindows || !persistencePath.Contains('\\') ?
persistencePath :
persistencePath.Replace('\\', Path.DirectorySeparatorChar);
}

This normalization also depends on PathExtensions.IsWindows which leads to solution file being inconsistently serialized on different OSs - the following code has different output based on which OS is running it:

using System.Text;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.VisualStudio.SolutionPersistence.Serializer;

var solutionModel = new SolutionModel();
solutionModel.AddProject(@"Core\ConsoleApp.csproj");

using var memoryStream = new MemoryStream();
await SolutionSerializers.SlnXml.SaveAsync(memoryStream, solutionModel, CancellationToken.None);
memoryStream.Position = 0;
using var reader = new StreamReader(memoryStream, new UTF8Encoding(false), leaveOpen: true);

Console.WriteLine(reader.ReadToEnd());

On macOS:

<Solution>
  <Project Path="Core\ConsoleApp.csproj" />
</Solution>

On Windows:

<Solution>
  <Project Path="Core/ConsoleApp.csproj" />
</Solution>

Note that the directory separator got normalized to / on Windows but not on macOS - unless you normalize the paths yourself before adding a project to the solution model, the solution file will depend on the OS of the user

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions