From d47f69cc1b75d7a9a5126828ca52307e53d28884 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:10:21 +0000 Subject: [PATCH 01/13] Add slnf support to dotnet sln add/remove/list commands and dotnet new slnf template Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../dotnet/Commands/CliCommandStrings.resx | 3 + .../Solution/Add/SolutionAddCommand.cs | 64 ++++++++++- .../Solution/Remove/SolutionRemoveCommand.cs | 44 +++++++- .../Commands/xlf/CliCommandStrings.cs.xlf | 5 + .../Commands/xlf/CliCommandStrings.de.xlf | 5 + .../Commands/xlf/CliCommandStrings.es.xlf | 5 + .../Commands/xlf/CliCommandStrings.fr.xlf | 5 + .../Commands/xlf/CliCommandStrings.it.xlf | 5 + .../Commands/xlf/CliCommandStrings.ja.xlf | 5 + .../Commands/xlf/CliCommandStrings.ko.xlf | 5 + .../Commands/xlf/CliCommandStrings.pl.xlf | 5 + .../Commands/xlf/CliCommandStrings.pt-BR.xlf | 5 + .../Commands/xlf/CliCommandStrings.ru.xlf | 5 + .../Commands/xlf/CliCommandStrings.tr.xlf | 5 + .../xlf/CliCommandStrings.zh-Hans.xlf | 5 + .../xlf/CliCommandStrings.zh-Hant.xlf | 5 + src/Cli/dotnet/SlnfFileHelper.cs | 106 ++++++++++++++++++ .../content/Solution/Solution1.slnf | 6 + .../.template.config/template.json | 29 +++++ .../SolutionFilter/SolutionFilter1.slnf | 6 + 20 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 src/Cli/dotnet/SlnfFileHelper.cs create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 3a3fab487f89..bc7f8720707e 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -2698,4 +2698,7 @@ Proceed? Received 'ExecutionId' of value '{0}' for message '{1}' while the 'ExecutionId' received of the handshake message was '{2}'. {Locked="ExecutionId"} + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + \ No newline at end of file diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index 338271e6c8cf..f6f4e2ff3003 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -45,7 +45,7 @@ public SolutionAddCommand(ParseResult parseResult) : base(parseResult) _solutionFolderPath = parseResult.GetValue(SolutionAddCommandParser.SolutionFolderOption); _includeReferences = parseResult.GetValue(SolutionAddCommandParser.IncludeReferencesOption); SolutionArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SolutionArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath); - _solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory); + _solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory, includeSolutionFilterFiles: true); } public override int Execute() @@ -64,8 +64,16 @@ public override int Execute() return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath; }); - // Add projects to the solution - AddProjectsToSolutionAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + // Check if we're working with a solution filter file + if (_solutionFileFullPath.HasExtension(".slnf")) + { + AddProjectsToSolutionFilterAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } + else + { + // Add projects to the solution + AddProjectsToSolutionAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } return 0; } @@ -224,4 +232,54 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio } } } + + private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectPaths, CancellationToken cancellationToken) + { + // Solution filter files don't support --in-root or --solution-folder options + if (_inRoot || !string.IsNullOrEmpty(_solutionFolderPath)) + { + throw new GracefulException(CliCommandStrings.SolutionFilterDoesNotSupportFolderOptions); + } + + // Load the filtered solution to get the parent solution path and existing projects + SolutionModel filteredSolution = SlnFileFactory.CreateFromFilteredSolutionFile(_solutionFileFullPath); + string parentSolutionPath = filteredSolution.Description!; // The parent solution path is stored in Description + + // Load the parent solution to validate projects exist in it + SolutionModel parentSolution = SlnFileFactory.CreateFromFileOrDirectory(parentSolutionPath); + + // Get existing projects in the filter + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + + // Get solution-relative paths for new projects + var newProjects = new List(); + foreach (var projectPath in projectPaths) + { + string parentSolutionRelativePath = Path.GetRelativePath(Path.GetDirectoryName(parentSolutionPath)!, projectPath); + + // Check if project exists in parent solution + var projectInParent = parentSolution.FindProject(parentSolutionRelativePath); + if (projectInParent is null) + { + Reporter.Error.WriteLine(CliStrings.ProjectNotFoundInTheSolution, parentSolutionRelativePath, parentSolutionPath); + continue; + } + + // Check if project is already in the filter + if (existingProjects.Contains(parentSolutionRelativePath)) + { + Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, _solutionFileFullPath, parentSolutionRelativePath); + continue; + } + + newProjects.Add(parentSolutionRelativePath); + Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, parentSolutionRelativePath); + } + + // Add new projects to the existing list and save + var allProjects = existingProjects.Concat(newProjects).OrderBy(p => p); + SlnfFileHelper.SaveSolutionFilter(_solutionFileFullPath, parentSolutionPath, allProjects); + + await Task.CompletedTask; + } } diff --git a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs index 36030bd22621..2c8184dc29f1 100644 --- a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs @@ -27,7 +27,7 @@ public SolutionRemoveCommand(ParseResult parseResult) : base(parseResult) public override int Execute() { - string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory); + string solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory, includeSolutionFilterFiles: true); if (_projects.Count == 0) { throw new GracefulException(CliStrings.SpecifyAtLeastOneProjectToRemove); @@ -43,7 +43,15 @@ public override int Execute() ? MsbuildProject.GetProjectFileFromDirectory(p).FullName : p)); - RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + // Check if we're working with a solution filter file + if (solutionFileFullPath.HasExtension(".slnf")) + { + RemoveProjectsFromSolutionFilterAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } + else + { + RemoveProjectsAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + } return 0; } catch (Exception ex) when (ex is not GracefulException) @@ -130,4 +138,36 @@ private static async Task RemoveProjectsAsync(string solutionFileFullPath, IEnum await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } + + private static async Task RemoveProjectsFromSolutionFilterAsync(string slnfFileFullPath, IEnumerable projectPaths, CancellationToken cancellationToken) + { + // Load the filtered solution to get the parent solution path and existing projects + SolutionModel filteredSolution = SlnFileFactory.CreateFromFilteredSolutionFile(slnfFileFullPath); + string parentSolutionPath = filteredSolution.Description!; // The parent solution path is stored in Description + + // Get existing projects in the filter + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + + // Remove specified projects + foreach (var projectPath in projectPaths) + { + // Normalize the path to be relative to parent solution + string normalizedPath = projectPath; + + // Try to find and remove the project + if (existingProjects.Remove(normalizedPath)) + { + Reporter.Output.WriteLine(CliStrings.ProjectRemovedFromTheSolution, normalizedPath); + } + else + { + Reporter.Output.WriteLine(CliStrings.ProjectNotFoundInTheSolution, normalizedPath); + } + } + + // Save updated filter + SlnfFileHelper.SaveSolutionFilter(slnfFileFullPath, parentSolutionPath, existingProjects.OrderBy(p => p)); + + await Task.CompletedTask; + } } diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index 9e6ea08c74b9..ad8f833f14c3 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -2991,6 +2991,11 @@ Cílem projektu je více architektur. Pomocí parametru {0} určete, která arch SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Parametry --solution-folder a --in-root nejdou použít společně; použijte jenom jeden z nich. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index 585628e6400b..89fa826c2902 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -2991,6 +2991,11 @@ Ihr Projekt verwendet mehrere Zielframeworks. Geben Sie über "{0}" an, welches SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Die Optionen "--solution-folder" und "--in-root" können nicht zusammen verwendet werden; verwenden Sie nur eine der Optionen. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index 3860162de57e..058d6eaab312 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -2991,6 +2991,11 @@ Su proyecto tiene como destino varias plataformas. Especifique la que quiere usa SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Las opciones --in-root y --solution-folder no se pueden usar juntas. Utilice solo una de ellas. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 82c93100c2a2..3b79fe3123c2 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -2991,6 +2991,11 @@ Votre projet cible plusieurs frameworks. Spécifiez le framework à exécuter à SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. N'utilisez pas en même temps les options --solution-folder et --in-root. Utilisez uniquement l'une des deux options. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index d5e325548a52..30c67f636e69 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -2991,6 +2991,11 @@ Il progetto è destinato a più framework. Specificare il framework da eseguire SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Non è possibile usare contemporaneamente le opzioni --solution-folder e --in-root. Usare una sola delle opzioni. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index d094f986fe57..9d803992cacc 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -2991,6 +2991,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder オプションと --in-root オプションを一緒に使用することはできません。いずれかのオプションだけを使用します。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index f818c10fae6d..59eba705be3d 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -2991,6 +2991,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder와 --in-root 옵션을 함께 사용할 수 없습니다. 옵션을 하나만 사용하세요. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 100304d2eee0..5d3a6a7eccbe 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -2991,6 +2991,11 @@ Projekt ma wiele platform docelowych. Określ platformę do uruchomienia przy u SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Opcji --solution-folder i --in-root nie można używać razem; użyj tylko jednej z tych opcji. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index 2e8c726a75a1..dd72e552e624 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -2991,6 +2991,11 @@ Ele tem diversas estruturas como destino. Especifique que estrutura executar usa SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. As opções --solution-folder e --in-root não podem ser usadas juntas. Use somente uma das opções. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index 391f4d068e0c..50f8adb8ce0c 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -2991,6 +2991,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. Параметры --solution-folder и --in-root options не могут быть использованы одновременно; оставьте только один из параметров. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index f6ffea6d2984..497de32433e6 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -2991,6 +2991,11 @@ Projeniz birden fazla Framework'ü hedefliyor. '{0}' kullanarak hangi Framework' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder ve --in-root seçenekleri birlikte kullanılamaz; seçeneklerden yalnızca birini kullanın. diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 7878f6ad146e..543596274520 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -2991,6 +2991,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. --solution-folder 和 --in-root 选项不能一起使用;请仅使用其中一个选项。 diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index 5cc582623c6d..d5c34f5bab98 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -2991,6 +2991,11 @@ Your project targets multiple frameworks. Specify which framework to run using ' SLN_FILE + + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + Solution filter files (.slnf) do not support the --in-root or --solution-folder options. + + The --solution-folder and --in-root options cannot be used together; use only one of the options. 不可同時使用 --solution-folder 和 --in-root 選項; 請只使用其中一個選項。 diff --git a/src/Cli/dotnet/SlnfFileHelper.cs b/src/Cli/dotnet/SlnfFileHelper.cs new file mode 100644 index 000000000000..368b930b5f13 --- /dev/null +++ b/src/Cli/dotnet/SlnfFileHelper.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Cli; + +/// +/// Utilities for working with solution filter (.slnf) files +/// +public static class SlnfFileHelper +{ + private class SlnfSolution + { + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonPropertyName("projects")] + public List Projects { get; set; } = new(); + } + + private class SlnfRoot + { + [JsonPropertyName("solution")] + public SlnfSolution Solution { get; set; } = new(); + } + + /// + /// Creates a new solution filter file + /// + /// Path to the solution filter file to create + /// Path to the parent solution file + /// List of project paths to include (relative to the parent solution) + public static void CreateSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects = null) + { + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)); + var parentSolutionFullPath = Path.GetFullPath(parentSolutionPath, slnfDirectory); + var relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionFullPath); + + // Normalize path separators to backslashes (as per slnf format) + relativeSolutionPath = relativeSolutionPath.Replace(Path.DirectorySeparatorChar, '\\'); + + var root = new SlnfRoot + { + Solution = new SlnfSolution + { + Path = relativeSolutionPath, + Projects = projects?.Select(p => p.Replace(Path.DirectorySeparatorChar, '\\')).ToList() ?? new List() + } + }; + + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + var json = JsonSerializer.Serialize(root, options); + File.WriteAllText(slnfPath, json); + } + + /// + /// Saves a solution filter file with the given projects + /// + /// Path to the solution filter file + /// Path to the parent solution (stored in the slnf file) + /// List of project paths (relative to the parent solution) + public static void SaveSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects) + { + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)); + + // Normalize the parent solution path to be relative to the slnf file + var relativeSolutionPath = parentSolutionPath; + if (Path.IsPathRooted(parentSolutionPath)) + { + relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionPath); + } + + // Normalize path separators to backslashes (as per slnf format) + relativeSolutionPath = relativeSolutionPath.Replace(Path.DirectorySeparatorChar, '\\'); + + var root = new SlnfRoot + { + Solution = new SlnfSolution + { + Path = relativeSolutionPath, + Projects = projects.Select(p => p.Replace(Path.DirectorySeparatorChar, '\\')).ToList() + } + }; + + var options = new JsonSerializerOptions + { + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never + }; + + var json = JsonSerializer.Serialize(root, options); + File.WriteAllText(slnfPath, json); + } +} diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf new file mode 100644 index 000000000000..0578829ee51e --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "Solution1.slnx", + "projects": [] + } +} diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json new file mode 100644 index 000000000000..7f33956aab45 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/template.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Solution" + ], + "name": "Solution Filter File", + "generatorVersions": "[1.0.0.0-*)", + "description": "Create a solution filter file that references a parent solution", + "groupIdentity": "ItemSolutionFilter", + "precedence": "100", + "identity": "Microsoft.Standard.QuickStarts.SolutionFilter", + "shortName": [ + "slnf", + "solutionfilter" + ], + "sourceName": "SolutionFilter1", + "symbols": { + "ParentSolution": { + "type": "parameter", + "displayName": "Parent solution file", + "description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension).", + "datatype": "string", + "defaultValue": "SolutionFilter1.slnx", + "replaces": "SolutionFilter1.slnx" + } + }, + "defaultName": "SolutionFilter1" +} diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf new file mode 100644 index 000000000000..d633922fb216 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/SolutionFilter1.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "SolutionFilter1.slnx", + "projects": [] + } +} From 146ee68a636105adc2f5d826a21536951b9e3907 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:15:20 +0000 Subject: [PATCH 02/13] Add tests for slnf add/remove functionality Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../ProjectToolsCommandResolver.cs | 2 +- src/Cli/dotnet/Commands/Build/BuildCommand.cs | 2 +- src/Cli/dotnet/Commands/Clean/CleanCommand.cs | 4 +- .../dotnet/Commands/Format/FormatCommand.cs | 2 +- .../Hidden/Complete/CompleteCommand.cs | 2 +- .../InternalReportInstallSuccessCommand.cs | 2 +- .../dotnet/Commands/MSBuild/MSBuildCommand.cs | 4 +- src/Cli/dotnet/Commands/Pack/PackCommand.cs | 6 +- .../Package/List/PackageListCommand.cs | 4 +- .../Package/Search/PackageSearchCommand.cs | 2 +- .../dotnet/Commands/Publish/PublishCommand.cs | 2 +- .../dotnet/Commands/Restore/RestoreCommand.cs | 2 +- .../Commands/Restore/RestoringCommand.cs | 8 +-- .../LaunchSettings/LaunchSettingsManager.cs | 4 +- src/Cli/dotnet/Commands/Run/RunTelemetry.cs | 2 +- .../Migrate/SolutionMigrateCommand.cs | 4 +- .../Solution/Remove/SolutionRemoveCommand.cs | 4 +- src/Cli/dotnet/Commands/Store/StoreCommand.cs | 2 +- .../Test/MTP/Terminal/TerminalTestReporter.cs | 4 +- .../Test/MTP/TestApplicationActionQueue.cs | 2 +- .../Commands/Test/VSTest/TestCommand.cs | 9 +-- .../Test/VSTest/VSTestForwardingApp.cs | 2 +- .../ToolInstallGlobalOrToolPathCommand.cs | 20 +++--- .../Tool/Install/ToolInstallLocalCommand.cs | 2 +- .../Commands/Tool/List/ToolListJsonHelper.cs | 12 ++-- .../Tool/Restore/ToolPackageRestorer.cs | 2 +- .../ToolUninstallGlobalOrToolPathCommand.cs | 2 +- .../ToolUpdateGlobalOrToolPathCommand.cs | 6 +- .../History/WorkloadHistoryCommand.cs | 4 +- .../Restore/WorkloadRestoreCommand.cs | 2 +- .../Commands/Workload/WorkloadCommandBase.cs | 2 +- .../Workload/WorkloadCommandParser.cs | 2 +- src/Cli/dotnet/CommonOptions.cs | 2 +- src/Cli/dotnet/DotNetCommandFactory.cs | 2 +- .../Extensions/CommonOptionsExtensions.cs | 2 +- .../INuGetPackageDownloader.cs | 2 +- .../NuGetPackageDownloader.cs | 4 +- .../dotnet/ReleasePropertyProjectLocator.cs | 3 +- src/Cli/dotnet/SlnfFileHelper.cs | 4 +- src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs | 4 +- .../Telemetry/EnvironmentDetectionRule.cs | 8 +-- .../Telemetry/ILLMEnvironmentDetector.cs | 2 +- .../LLMEnvironmentDetectorForTelemetry.cs | 2 +- src/Cli/dotnet/Telemetry/Telemetry.cs | 4 +- .../dotnet/ToolPackage/ToolConfiguration.cs | 2 +- .../localize/templatestrings.cs.json | 7 ++ .../localize/templatestrings.de.json | 7 ++ .../localize/templatestrings.en.json | 7 ++ .../localize/templatestrings.es.json | 7 ++ .../localize/templatestrings.fr.json | 7 ++ .../localize/templatestrings.it.json | 7 ++ .../localize/templatestrings.ja.json | 7 ++ .../localize/templatestrings.ko.json | 7 ++ .../localize/templatestrings.pl.json | 7 ++ .../localize/templatestrings.pt-BR.json | 7 ++ .../localize/templatestrings.ru.json | 7 ++ .../localize/templatestrings.tr.json | 7 ++ .../localize/templatestrings.zh-Hans.json | 7 ++ .../localize/templatestrings.zh-Hant.json | 7 ++ .../TestAppWithSlnfFiles/App.slnf | 8 +++ .../TestAppWithSlnfFiles/App.slnx | 5 ++ .../TestAppWithSlnfFiles/src/App/App.csproj | 6 ++ .../TestAppWithSlnfFiles/src/Lib/Lib.csproj | 5 ++ .../test/AppTests/AppTests.csproj | 9 +++ .../Solution/Add/GivenDotnetSlnAdd.cs | 66 +++++++++++++++++++ 65 files changed, 284 insertions(+), 83 deletions(-) create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj create mode 100644 test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj diff --git a/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectToolsCommandResolver.cs b/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectToolsCommandResolver.cs index 2f8bb7badd98..be3d9294b176 100644 --- a/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectToolsCommandResolver.cs +++ b/src/Cli/dotnet/CommandFactory/CommandResolution/ProjectToolsCommandResolver.cs @@ -385,7 +385,7 @@ internal void GenerateDepsJsonFile( string? stdOut; string? stdErr; - var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments([..args], CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, BuildCommandParser.TargetOption, BuildCommandParser.VerbosityOption); + var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments([.. args], CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, BuildCommandParser.TargetOption, BuildCommandParser.VerbosityOption); var forwardingAppWithoutLogging = new MSBuildForwardingAppWithoutLogging(msbuildArgs, msBuildExePath); if (forwardingAppWithoutLogging.ExecuteMSBuildOutOfProc) { diff --git a/src/Cli/dotnet/Commands/Build/BuildCommand.cs b/src/Cli/dotnet/Commands/Build/BuildCommand.cs index 871ead794e84..4d8e425dace9 100644 --- a/src/Cli/dotnet/Commands/Build/BuildCommand.cs +++ b/src/Cli/dotnet/Commands/Build/BuildCommand.cs @@ -12,7 +12,7 @@ public static class BuildCommand { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "build", ..args]); + var parseResult = Parser.Parse(["dotnet", "build", .. args]); return FromParseResult(parseResult, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs index 1290b8b68cfd..7c5516b05dd3 100644 --- a/src/Cli/dotnet/Commands/Clean/CleanCommand.cs +++ b/src/Cli/dotnet/Commands/Clean/CleanCommand.cs @@ -13,7 +13,7 @@ public class CleanCommand(MSBuildArgs msbuildArgs, string? msbuildPath = null) : { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var result = Parser.Parse(["dotnet", "clean", ..args]); + var result = Parser.Parse(["dotnet", "clean", .. args]); return FromParseResult(result, msbuildPath); } @@ -33,7 +33,7 @@ public static CommandBase FromParseResult(ParseResult result, string? msbuildPat NoWriteBuildMarkers = true, }, static (msbuildArgs, msbuildPath) => new CleanCommand(msbuildArgs, msbuildPath), - [ CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CleanCommandParser.TargetOption, CleanCommandParser.VerbosityOption ], + [CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CleanCommandParser.TargetOption, CleanCommandParser.VerbosityOption], result, msbuildPath ); diff --git a/src/Cli/dotnet/Commands/Format/FormatCommand.cs b/src/Cli/dotnet/Commands/Format/FormatCommand.cs index 9ee9296172fa..d6629af67720 100644 --- a/src/Cli/dotnet/Commands/Format/FormatCommand.cs +++ b/src/Cli/dotnet/Commands/Format/FormatCommand.cs @@ -13,7 +13,7 @@ public class FormatCommand(IEnumerable argsToForward) : FormatForwarding { public static FormatCommand FromArgs(string[] args) { - var result = Parser.Parse(["dotnet", "format", ..args]); + var result = Parser.Parse(["dotnet", "format", .. args]); return FromParseResult(result); } diff --git a/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs b/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs index 5cdf66cfca6e..33904941b817 100644 --- a/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/Complete/CompleteCommand.cs @@ -19,7 +19,7 @@ public static int Run(ParseResult parseResult) public static int RunWithReporter(string[] args, IReporter reporter) { - var result = Parser.Parse(["dotnet", "complete", ..args]); + var result = Parser.Parse(["dotnet", "complete", .. args]); return RunWithReporter(result, reporter); } diff --git a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs index 744289023948..bed479d01816 100644 --- a/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs +++ b/src/Cli/dotnet/Commands/Hidden/InternalReportInstallSuccess/InternalReportInstallSuccessCommand.cs @@ -25,7 +25,7 @@ public static int Run(ParseResult parseResult) public static void ProcessInputAndSendTelemetry(string[] args, ITelemetry telemetry) { - var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", ..args]); + var result = Parser.Parse(["dotnet", "internal-reportinstallsuccess", .. args]); ProcessInputAndSendTelemetry(result, telemetry); } diff --git a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs b/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs index cf0b7e06c660..5deae21cb609 100644 --- a/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs +++ b/src/Cli/dotnet/Commands/MSBuild/MSBuildCommand.cs @@ -10,11 +10,11 @@ namespace Microsoft.DotNet.Cli.Commands.MSBuild; public class MSBuildCommand( IEnumerable msbuildArgs, string? msbuildPath = null -) : MSBuildForwardingApp(MSBuildArgs.AnalyzeMSBuildArguments([..msbuildArgs], CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, MSBuildCommandParser.TargetOption, CommonOptions.VerbosityOption()), msbuildPath, includeLogo: true) +) : MSBuildForwardingApp(MSBuildArgs.AnalyzeMSBuildArguments([.. msbuildArgs], CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, MSBuildCommandParser.TargetOption, CommonOptions.VerbosityOption()), msbuildPath, includeLogo: true) { public static MSBuildCommand FromArgs(string[] args, string? msbuildPath = null) { - var result = Parser.Parse(["dotnet", "msbuild", ..args]); + var result = Parser.Parse(["dotnet", "msbuild", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 1e88f22688f5..3d574c30bf18 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -24,7 +24,7 @@ public class PackCommand( { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "pack", ..args]); + var parseResult = Parser.Parse(["dotnet", "pack", .. args]); return FromParseResult(parseResult, msbuildPath); } @@ -92,14 +92,14 @@ public static int RunPackCommand(ParseResult parseResult) if (args.Count != 1) { - Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); + Console.Error.WriteLine(CliStrings.PackCmd_OneNuspecAllowed); return 1; } var nuspecPath = args[0]; var packArgs = new PackArgs() - { + { Logger = new NuGetConsoleLogger(), Exclude = new List(), OutputDirectory = parseResult.GetValue(PackCommandParser.OutputOption), diff --git a/src/Cli/dotnet/Commands/Package/List/PackageListCommand.cs b/src/Cli/dotnet/Commands/Package/List/PackageListCommand.cs index 27520377cad2..7e340ac81fa7 100644 --- a/src/Cli/dotnet/Commands/Package/List/PackageListCommand.cs +++ b/src/Cli/dotnet/Commands/Package/List/PackageListCommand.cs @@ -4,12 +4,12 @@ #nullable disable using System.CommandLine; +using System.Globalization; using Microsoft.DotNet.Cli.Commands.Hidden.List; +using Microsoft.DotNet.Cli.Commands.MSBuild; using Microsoft.DotNet.Cli.Commands.NuGet; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; -using System.Globalization; -using Microsoft.DotNet.Cli.Commands.MSBuild; namespace Microsoft.DotNet.Cli.Commands.Package.List; diff --git a/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs b/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs index 4317f96329be..8bbfd5261cdc 100644 --- a/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs @@ -3,9 +3,9 @@ #nullable disable +using System.CommandLine; using Microsoft.DotNet.Cli.Commands.NuGet; using Microsoft.DotNet.Cli.Extensions; -using System.CommandLine; namespace Microsoft.DotNet.Cli.Commands.Package.Search; diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index 45dc32c84300..dfafac3d4807 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -21,7 +21,7 @@ private PublishCommand( public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "publish", ..args]); + var parseResult = Parser.Parse(["dotnet", "publish", .. args]); return FromParseResult(parseResult); } diff --git a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs index 6eb650b0e261..bd685b2b6ec2 100644 --- a/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Restore/RestoreCommand.cs @@ -13,7 +13,7 @@ public static class RestoreCommand { public static CommandBase FromArgs(string[] args, string? msbuildPath = null) { - var result = Parser.Parse(["dotnet", "restore", ..args]); + var result = Parser.Parse(["dotnet", "restore", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs b/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs index dd2e961c1524..ea92c35ab063 100644 --- a/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs +++ b/src/Cli/dotnet/Commands/Restore/RestoringCommand.cs @@ -37,7 +37,7 @@ public RestoringCommand( string? msbuildPath = null, string? userProfileDir = null, bool? advertiseWorkloadUpdates = null) - : base(GetCommandArguments(msbuildArgs, noRestore), msbuildPath) + : base(GetCommandArguments(msbuildArgs, noRestore), msbuildPath) { userProfileDir = CliFolderPathCalculator.DotnetUserProfileFolderPath; Task.Run(() => WorkloadManifestUpdater.BackgroundUpdateAdvertisingManifestsAsync(userProfileDir)); @@ -122,13 +122,13 @@ private static MSBuildArgs GetCommandArguments( ReadOnlyDictionary restoreProperties = msbuildArgs.GlobalProperties? .Where(kvp => !IsPropertyExcludedFromRestore(kvp.Key))? - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase) is { } filteredList ? new(filteredList): ReadOnlyDictionary.Empty; + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase) is { } filteredList ? new(filteredList) : ReadOnlyDictionary.Empty; var restoreMSBuildArgs = MSBuildArgs.FromProperties(RestoreOptimizationProperties) .CloneWithAdditionalTargets("Restore") .CloneWithExplicitArgs([.. newArgumentsToAdd, .. existingArgumentsToForward]) .CloneWithAdditionalProperties(restoreProperties); - if (msbuildArgs.Verbosity is {} verbosity) + if (msbuildArgs.Verbosity is { } verbosity) { restoreMSBuildArgs = restoreMSBuildArgs.CloneWithVerbosity(verbosity); } @@ -175,7 +175,7 @@ private static bool HasPropertyToExcludeFromRestore(MSBuildArgs msbuildArgs) private static readonly List FlagsThatTriggerSilentSeparateRestore = [.. ComputeFlags(FlagsThatTriggerSilentRestore)]; - private static readonly List PropertiesToExcludeFromSeparateRestore = [ .. PropertiesToExcludeFromRestore ]; + private static readonly List PropertiesToExcludeFromSeparateRestore = [.. PropertiesToExcludeFromRestore]; /// /// We investigate the arguments we're about to send to a separate restore call and filter out diff --git a/src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs b/src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs index a1776027476d..3e4c7b2bd53e 100644 --- a/src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs +++ b/src/Cli/dotnet/Commands/Run/LaunchSettings/LaunchSettingsManager.cs @@ -84,7 +84,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett { if (prop.Value.TryGetProperty(CommandNameKey, out var commandNameElement) && commandNameElement.ValueKind == JsonValueKind.String) { - if (commandNameElement.GetString() is { } commandNameElementKey && _providers.ContainsKey(commandNameElementKey)) + if (commandNameElement.GetString() is { } commandNameElementKey && _providers.ContainsKey(commandNameElementKey)) { profileObject = prop.Value; break; @@ -120,7 +120,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett } } - private static bool TryLocateHandler(string? commandName, [NotNullWhen(true)]out ILaunchSettingsProvider? provider) + private static bool TryLocateHandler(string? commandName, [NotNullWhen(true)] out ILaunchSettingsProvider? provider) { if (commandName == null) { diff --git a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs index 35e13b2d3fd2..47fae9a27f85 100644 --- a/src/Cli/dotnet/Commands/Run/RunTelemetry.cs +++ b/src/Cli/dotnet/Commands/Run/RunTelemetry.cs @@ -234,4 +234,4 @@ private static bool IsDefaultProfile(string? profileName) // The default profile name at this point is "(Default)" return profileName.Equals("(Default)", StringComparison.OrdinalIgnoreCase); } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs b/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs index 074431c8981b..8c445a02d87f 100644 --- a/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Migrate/SolutionMigrateCommand.cs @@ -29,7 +29,9 @@ public override int Execute() { ConvertToSlnxAsync(slnFileFullPath, slnxFileFullPath, CancellationToken.None).Wait(); return 0; - } catch (Exception ex) { + } + catch (Exception ex) + { throw new GracefulException(ex.Message, ex); } } diff --git a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs index 2c8184dc29f1..f84db1da64ac 100644 --- a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs @@ -132,7 +132,7 @@ private static async Task RemoveProjectsAsync(string solutionFileFullPath, IEnum { solution.RemoveFolder(folder); // After removal, adjust index and continue to avoid skipping folders after removal - i--; + i--; } } @@ -153,7 +153,7 @@ private static async Task RemoveProjectsFromSolutionFilterAsync(string slnfFileF { // Normalize the path to be relative to parent solution string normalizedPath = projectPath; - + // Try to find and remove the project if (existingProjects.Remove(normalizedPath)) { diff --git a/src/Cli/dotnet/Commands/Store/StoreCommand.cs b/src/Cli/dotnet/Commands/Store/StoreCommand.cs index 0c7846c513e2..f02b52b641dc 100644 --- a/src/Cli/dotnet/Commands/Store/StoreCommand.cs +++ b/src/Cli/dotnet/Commands/Store/StoreCommand.cs @@ -19,7 +19,7 @@ private StoreCommand(IEnumerable msbuildArgs, string msbuildPath = null) public static StoreCommand FromArgs(string[] args, string msbuildPath = null) { - var result = Parser.Parse(["dotnet", "store", ..args]); + var result = Parser.Parse(["dotnet", "store", .. args]); return FromParseResult(result, msbuildPath); } diff --git a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs index 3e320fa8a06b..766acd1d33ec 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/Terminal/TerminalTestReporter.cs @@ -1,12 +1,12 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; -using Microsoft.TemplateEngine.Cli.Help; using System.Globalization; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; -using Microsoft.Testing.Platform.OutputDevice.Terminal; using Microsoft.DotNet.Cli.Commands.Test.IPC.Models; +using Microsoft.TemplateEngine.Cli.Help; +using Microsoft.Testing.Platform.OutputDevice.Terminal; namespace Microsoft.DotNet.Cli.Commands.Test.Terminal; diff --git a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs index 41ee319317e7..4496703ace28 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/TestApplicationActionQueue.cs @@ -78,7 +78,7 @@ private async Task Read(BuildOptions buildOptions, TestOptions testOptions, Term { result = ExitCode.GenericFailure; } - + lock (_lock) { if (_aggregateExitCode is null) diff --git a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs index 51df89df08e0..53d4824d1c12 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/TestCommand.cs @@ -154,7 +154,7 @@ private static int ForwardToVSTestConsole(ParseResult parseResult, string[] args public static TestCommand FromArgs(string[] args, string? testSessionCorrelationId = null, string? msbuildPath = null) { - var parseResult = Parser.Parse(["dotnet", "test", ..args]); + var parseResult = Parser.Parse(["dotnet", "test", .. args]); // settings parameters are after -- (including --), these should not be considered by the parser string[] settings = [.. args.SkipWhile(a => a != "--")]; @@ -240,9 +240,10 @@ private static TestCommand FromParseResult(ParseResult result, string[] settings } } - + Dictionary variables = VSTestForwardingApp.GetVSTestRootVariables(); - foreach (var (rootVariableName, rootValue) in variables) { + foreach (var (rootVariableName, rootValue) in variables) + { testCommand.EnvironmentVariable(rootVariableName, rootValue); VSTestTrace.SafeWriteTrace(() => $"Root variable set {rootVariableName}:{rootValue}"); } @@ -304,7 +305,7 @@ private static bool ContainsBuiltTestSources(string[] args) if (arg.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || arg.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { var previousArg = i > 0 ? args[i - 1] : null; - if (previousArg != null && CommonOptions.PropertiesOption.Aliases.Contains(previousArg)) + if (previousArg != null && CommonOptions.PropertiesOption.Aliases.Contains(previousArg)) { return false; } diff --git a/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs b/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs index fb81e15466f9..26a021485c97 100644 --- a/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs +++ b/src/Cli/dotnet/Commands/Test/VSTest/VSTestForwardingApp.cs @@ -20,7 +20,7 @@ public VSTestForwardingApp(IEnumerable argsToForward) WithEnvironmentVariable(rootVariableName, rootValue); VSTestTrace.SafeWriteTrace(() => $"Root variable set {rootVariableName}:{rootValue}"); } - + VSTestTrace.SafeWriteTrace(() => $"Forwarding to '{GetVSTestExePath()}' with args \"{argsToForward?.Aggregate((a, b) => $"{a} | {b}")}\""); } diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index c465e20372e5..431b92f2c654 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -5,20 +5,20 @@ using System.CommandLine; using System.Transactions; +using Microsoft.DotNet.Cli.Commands.Tool.Common; +using Microsoft.DotNet.Cli.Commands.Tool.List; +using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; +using Microsoft.DotNet.Cli.Commands.Tool.Update; +using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; +using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Frameworks; using NuGet.Versioning; -using Microsoft.DotNet.Cli.Utils.Extensions; -using Microsoft.DotNet.Cli.Extensions; -using Microsoft.DotNet.Cli.ShellShim; -using Microsoft.DotNet.Cli.Commands.Tool.Update; -using Microsoft.DotNet.Cli.Commands.Tool.Common; -using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; -using Microsoft.DotNet.Cli.Commands.Tool.List; namespace Microsoft.DotNet.Cli.Commands.Tool.Install; @@ -187,7 +187,7 @@ private int ExecuteInstallCommand(PackageId packageId) { _reporter.WriteLine(string.Format(CliCommandStrings.ToolAlreadyInstalled, oldPackageNullable.Id, oldPackageNullable.Version.ToNormalizedString()).Green()); return 0; - } + } } TransactionalAction.Run(() => @@ -318,7 +318,7 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag { try { - uninstallAction(); + uninstallAction(); } catch (Exception ex) when (ToolUninstallCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex)) @@ -396,7 +396,7 @@ private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstal { _reporter.WriteLine( string.Format( - + newInstalledPackage.Version.IsPrerelease ? CliCommandStrings.UpdateSucceededPreVersionNoChange : CliCommandStrings.UpdateSucceededStableVersionNoChange, newInstalledPackage.Id, newInstalledPackage.Version).Green()); diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs index 87fb7860f992..e0bf8ccd3247 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallLocalCommand.cs @@ -83,7 +83,7 @@ public override int Execute() } else { - return ExecuteInstallCommand((PackageId) _packageId); + return ExecuteInstallCommand((PackageId)_packageId); } } diff --git a/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs b/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs index 2ff9552ceeca..914f19efe192 100644 --- a/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs +++ b/src/Cli/dotnet/Commands/Tool/List/ToolListJsonHelper.cs @@ -10,12 +10,12 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.List; internal sealed class VersionedDataContract { - /// - /// The version of the JSON format for dotnet tool list. - /// + /// + /// The version of the JSON format for dotnet tool list. + /// [JsonPropertyName("version")] public int Version { get; init; } = 1; - + [JsonPropertyName("data")] public required TContract Data { get; init; } } @@ -24,10 +24,10 @@ internal class ToolListJsonContract { [JsonPropertyName("packageId")] public required string PackageId { get; init; } - + [JsonPropertyName("version")] public required string Version { get; init; } - + [JsonPropertyName("commands")] public required string[] Commands { get; init; } } diff --git a/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs b/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs index b1c3b3f4ed52..1377a97cb006 100644 --- a/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs +++ b/src/Cli/dotnet/Commands/Tool/Restore/ToolPackageRestorer.cs @@ -109,7 +109,7 @@ private static bool ManifestCommandMatchesActualInPackage( IReadOnlyList toolPackageCommands) { ToolCommandName[] commandsFromPackage = [.. toolPackageCommands.Select(t => t.Name)]; -return !commandsFromManifest.Any(cmd => !commandsFromPackage.Contains(cmd)) && !commandsFromPackage.Any(cmd => !commandsFromManifest.Contains(cmd)); + return !commandsFromManifest.Any(cmd => !commandsFromPackage.Contains(cmd)) && !commandsFromPackage.Any(cmd => !commandsFromManifest.Contains(cmd)); } public bool PackageHasBeenRestored( diff --git a/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs index 58db9f55cc04..6db95e91941a 100644 --- a/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Uninstall/ToolUninstallGlobalOrToolPathCommand.cs @@ -73,7 +73,7 @@ public override int Execute() TransactionalAction.Run(() => { shellShimRepository.RemoveShim(package.Command); - + toolPackageUninstaller.Uninstall(package.PackageDirectory); }); diff --git a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs index 4c73cebd76f0..2d4c881bbc83 100644 --- a/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Update/ToolUpdateGlobalOrToolPathCommand.cs @@ -4,12 +4,12 @@ #nullable disable using System.CommandLine; +using Microsoft.DotNet.Cli.Commands.Tool.Install; +using Microsoft.DotNet.Cli.ShellShim; +using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; -using Microsoft.DotNet.Cli.ToolPackage; using CreateShellShimRepository = Microsoft.DotNet.Cli.Commands.Tool.Install.CreateShellShimRepository; -using Microsoft.DotNet.Cli.ShellShim; -using Microsoft.DotNet.Cli.Commands.Tool.Install; namespace Microsoft.DotNet.Cli.Commands.Tool.Update; diff --git a/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs b/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs index ceebc46404a9..cbb727effd59 100644 --- a/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/History/WorkloadHistoryCommand.cs @@ -4,11 +4,11 @@ #nullable disable using System.CommandLine; +using Microsoft.Deployment.DotNet.Releases; +using Microsoft.DotNet.Cli.Commands.Workload.Install; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Sdk.WorkloadManifestReader; -using Microsoft.Deployment.DotNet.Releases; -using Microsoft.DotNet.Cli.Commands.Workload.Install; namespace Microsoft.DotNet.Cli.Commands.Workload.History; diff --git a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs index 1dbc16110933..e1f64e74fb98 100644 --- a/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Restore/WorkloadRestoreCommand.cs @@ -60,7 +60,7 @@ public override int Execute() }); workloadInstaller.Shutdown(); - + return 0; } diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs index 44b441349be3..83c3622afd18 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs @@ -96,7 +96,7 @@ public WorkloadCommandBase( Verbosity = verbosityOptions == null ? parseResult.GetValue(CommonOptions.VerbosityOption(VerbosityOptions.normal)) - : parseResult.GetValue(verbosityOptions) ; + : parseResult.GetValue(verbosityOptions); ILogger nugetLogger = Verbosity.IsDetailedOrDiagnostic() ? new NuGetConsoleLogger() : new NullLogger(); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs index 3c6e0bb43c6d..79775f7664ac 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandParser.cs @@ -20,8 +20,8 @@ using Microsoft.DotNet.Cli.Utils; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.TemplateEngine.Cli.Commands; -using IReporter = Microsoft.DotNet.Cli.Utils.IReporter; using Command = System.CommandLine.Command; +using IReporter = Microsoft.DotNet.Cli.Utils.IReporter; namespace Microsoft.DotNet.Cli.Commands.Workload; diff --git a/src/Cli/dotnet/CommonOptions.cs b/src/Cli/dotnet/CommonOptions.cs index aa1730a23525..2b0c376c906f 100644 --- a/src/Cli/dotnet/CommonOptions.cs +++ b/src/Cli/dotnet/CommonOptions.cs @@ -348,7 +348,7 @@ public static ForwardedOption InteractiveOption(bool acceptArgument = fals }; public static readonly Option> EnvOption = CreateEnvOption(CliStrings.CmdEnvironmentVariableDescription); - + public static readonly Option> TestEnvOption = CreateEnvOption(CliStrings.CmdTestEnvironmentVariableDescription); private static IReadOnlyDictionary ParseEnvironmentVariables(ArgumentResult argumentResult) diff --git a/src/Cli/dotnet/DotNetCommandFactory.cs b/src/Cli/dotnet/DotNetCommandFactory.cs index ea5eb912e8f6..dcb70b05e6c9 100644 --- a/src/Cli/dotnet/DotNetCommandFactory.cs +++ b/src/Cli/dotnet/DotNetCommandFactory.cs @@ -38,7 +38,7 @@ private static bool TryGetBuiltInCommand(string commandName, out Func Parser.Invoke([commandName, ..args]); + commandFunc = (args) => Parser.Invoke([commandName, .. args]); return true; } commandFunc = null; diff --git a/src/Cli/dotnet/Extensions/CommonOptionsExtensions.cs b/src/Cli/dotnet/Extensions/CommonOptionsExtensions.cs index 9254bbd73b77..a225056f02f8 100644 --- a/src/Cli/dotnet/Extensions/CommonOptionsExtensions.cs +++ b/src/Cli/dotnet/Extensions/CommonOptionsExtensions.cs @@ -4,8 +4,8 @@ #nullable disable using Microsoft.Build.Framework; -using Microsoft.Extensions.Logging; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Cli.Extensions; diff --git a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs index a5e54ba06bb9..0c606c61dbf7 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/INuGetPackageDownloader.cs @@ -43,4 +43,4 @@ Task GetBestPackageVersionAsync(PackageId packageId, Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync(PackageId packageId, VersionRange versionRange, PackageSourceLocation packageSourceLocation = null); -} +} diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index a311e88c646d..a0ce16fe6d0b 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -75,7 +75,7 @@ public NuGetPackageDownloader( _retryTimer = timer; _sourceRepositories = new(); // If windows or env variable is set, verify signatures - _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true + _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true : bool.TryParse(Environment.GetEnvironmentVariable(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification), out var shouldVerifySignature) ? shouldVerifySignature : OperatingSystem.IsLinux()); _cacheSettings = new SourceCacheContext @@ -122,7 +122,7 @@ public async Task DownloadPackageAsync(PackageId packageId, throw new ArgumentException($"Package download folder must be specified either via {nameof(NuGetPackageDownloader)} constructor or via {nameof(downloadFolder)} method argument."); } var pathResolver = new VersionFolderPathResolver(resolvedDownloadFolder); - + string nupkgPath = pathResolver.GetPackageFilePath(packageId.ToString(), resolvedPackageVersion); Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)); diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index e85fb9878d4c..7c03df034464 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -230,7 +230,8 @@ DependentCommandOptions commandOptions { return projectData; } - }; + } + ; return null; } diff --git a/src/Cli/dotnet/SlnfFileHelper.cs b/src/Cli/dotnet/SlnfFileHelper.cs index 368b930b5f13..ad19e1c5c7f1 100644 --- a/src/Cli/dotnet/SlnfFileHelper.cs +++ b/src/Cli/dotnet/SlnfFileHelper.cs @@ -74,14 +74,14 @@ public static void CreateSolutionFilter(string slnfPath, string parentSolutionPa public static void SaveSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects) { var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)); - + // Normalize the parent solution path to be relative to the slnf file var relativeSolutionPath = parentSolutionPath; if (Path.IsPathRooted(parentSolutionPath)) { relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionPath); } - + // Normalize path separators to backslashes (as per slnf format) relativeSolutionPath = relativeSolutionPath.Replace(Path.DirectorySeparatorChar, '\\'); diff --git a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs index 015af6723629..7960deb22cc7 100644 --- a/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs +++ b/src/Cli/dotnet/Telemetry/DevDeviceIDGetter.cs @@ -85,11 +85,11 @@ private static void CacheDeviceId(string deviceId) // Cache device Id in Windows registry matching the OS architecture using (RegistryKey baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64)) { - using(var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) + using (var key = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\DeveloperTools")) { if (key != null) { - key.SetValue("deviceid", deviceId); + key.SetValue("deviceid", deviceId); } } } diff --git a/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs index 5cd73f53abb8..5f1aab066131 100644 --- a/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs +++ b/src/Cli/dotnet/Telemetry/EnvironmentDetectionRule.cs @@ -33,7 +33,7 @@ public BooleanEnvironmentRule(params string[] variables) public override bool IsMatch() { - return _variables.Any(variable => + return _variables.Any(variable => bool.TryParse(Environment.GetEnvironmentVariable(variable), out bool value) && value); } } @@ -96,8 +96,8 @@ public EnvironmentDetectionRuleWithResult(T result, params string[] variables) /// The result value if the rule matches; otherwise, null. public T? GetResult() { - return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) - ? _result + return _variables.Any(variable => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(variable))) + ? _result : null; } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs index fe599569aa6c..1fb747d47ae5 100644 --- a/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs +++ b/src/Cli/dotnet/Telemetry/ILLMEnvironmentDetector.cs @@ -6,4 +6,4 @@ namespace Microsoft.DotNet.Cli.Telemetry; internal interface ILLMEnvironmentDetector { string? GetLLMEnvironment(); -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs index 16d13a6879e7..532e91a2bd0a 100644 --- a/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs +++ b/src/Cli/dotnet/Telemetry/LLMEnvironmentDetectorForTelemetry.cs @@ -20,4 +20,4 @@ internal class LLMEnvironmentDetectorForTelemetry : ILLMEnvironmentDetector var results = _detectionRules.Select(r => r.GetResult()).Where(r => r != null).ToArray(); return results.Length > 0 ? string.Join(", ", results) : null; } -} \ No newline at end of file +} diff --git a/src/Cli/dotnet/Telemetry/Telemetry.cs b/src/Cli/dotnet/Telemetry/Telemetry.cs index d9c3a59bd8a1..38f0d1c7ca19 100644 --- a/src/Cli/dotnet/Telemetry/Telemetry.cs +++ b/src/Cli/dotnet/Telemetry/Telemetry.cs @@ -258,6 +258,6 @@ static IDictionary Combine(IDictionary { eventMeasurements[measurement.Key] = measurement.Value; } - return eventMeasurements; - } + return eventMeasurements; + } } diff --git a/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs b/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs index 641c8c583a7c..9da8558f5384 100644 --- a/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs +++ b/src/Cli/dotnet/ToolPackage/ToolConfiguration.cs @@ -62,7 +62,7 @@ private static void EnsureNoLeadingDot(string commandName) } } - + public string CommandName { get; } public string ToolAssemblyEntryPoint { get; } diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.cs.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.de.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.en.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.es.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.fr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.it.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ja.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ko.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pl.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.pt-BR.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.ru.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.tr.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hans.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json new file mode 100644 index 000000000000..535e0d7b8229 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/localize/templatestrings.zh-Hant.json @@ -0,0 +1,7 @@ +{ + "author": "Microsoft", + "name": "Solution Filter File", + "description": "Create a solution filter file that references a parent solution", + "symbols/ParentSolution/displayName": "Parent solution file", + "symbols/ParentSolution/description": "The parent solution file (sln or slnx) that this filter references (default: same name with .slnx extension)." +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf new file mode 100644 index 000000000000..34cef9585f66 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnf @@ -0,0 +1,8 @@ +{ + "solution": { + "path": "App.slnx", + "projects": [ + "src\\App\\App.csproj" + ] + } +} diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx new file mode 100644 index 000000000000..54df61baa606 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/App.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj new file mode 100644 index 000000000000..0361aa8cd36d --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/App/App.csproj @@ -0,0 +1,6 @@ + + + Exe + net9.0 + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj new file mode 100644 index 000000000000..3043227ce00b --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/src/Lib/Lib.csproj @@ -0,0 +1,5 @@ + + + net9.0 + + diff --git a/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj new file mode 100644 index 000000000000..eb91b2d48523 --- /dev/null +++ b/test/TestAssets/TestProjects/TestAppWithSlnfFiles/test/AppTests/AppTests.csproj @@ -0,0 +1,9 @@ + + + net9.0 + + + + + + diff --git a/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs b/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs index 3f78a0896e87..b2b30acdecc9 100644 --- a/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs +++ b/test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs @@ -1313,5 +1313,71 @@ private string GetSolutionFileTemplateContents(string templateFileName) .Path; return File.ReadAllText(Path.Join(templateContentDirectory, templateFileName)); } + + // SLNF TESTS + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenAddingProjectToSlnfItAddsOnlyIfInParentSolution(string solutionCommand) + { + var projectDirectory = _testAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-Slnf-{solutionCommand}") + .WithSource() + .Path; + + var slnfFullPath = Path.Combine(projectDirectory, "App.slnf"); + + // Try to add Lib project which is in parent solution + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "add", Path.Combine("src", "Lib", "Lib.csproj")); + cmd.Should().Pass(); + cmd.StdOut.Should().Contain(string.Format(CliStrings.ProjectAddedToTheSolution, Path.Combine("src", "Lib", "Lib.csproj"))); + + // Verify the project was added to the slnf file + var slnfContent = File.ReadAllText(slnfFullPath); + slnfContent.Should().Contain("src\\\\Lib\\\\Lib.csproj"); + } + + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenRemovingProjectFromSlnfItRemovesSuccessfully(string solutionCommand) + { + var projectDirectory = _testAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-SlnfRemove-{solutionCommand}") + .WithSource() + .Path; + + var slnfFullPath = Path.Combine(projectDirectory, "App.slnf"); + + // Remove the App project from the filter + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "remove", Path.Combine("src", "App", "App.csproj")); + cmd.Should().Pass(); + cmd.StdOut.Should().Contain(string.Format(CliStrings.ProjectRemovedFromTheSolution, Path.Combine("src", "App", "App.csproj"))); + + // Verify the project was removed from the slnf file + var slnfContent = File.ReadAllText(slnfFullPath); + slnfContent.Should().NotContain("src\\\\App\\\\App.csproj"); + } + + [Theory] + [InlineData("sln")] + [InlineData("solution")] + public void WhenAddingProjectToSlnfWithInRootOptionItErrors(string solutionCommand) + { + var projectDirectory = _testAssetsManager + .CopyTestAsset("TestAppWithSlnfFiles", identifier: $"GivenDotnetSlnAdd-SlnfInRoot-{solutionCommand}") + .WithSource() + .Path; + + var cmd = new DotnetCommand(Log) + .WithWorkingDirectory(projectDirectory) + .Execute(solutionCommand, "App.slnf", "add", "--in-root", Path.Combine("src", "Lib", "Lib.csproj")); + cmd.Should().Fail(); + cmd.StdErr.Should().Contain(CliCommandStrings.SolutionFilterDoesNotSupportFolderOptions); + } } } From b8b880077e420ab649bb340cf56ff0bc2d41cf8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Oct 2025 21:22:34 +0000 Subject: [PATCH 03/13] Fix null reference issues in slnf handling and improve path handling Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs | 7 ++++--- src/Cli/dotnet/SlnfFileHelper.cs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index f6f4e2ff3003..d846c9bf1f3b 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -58,11 +58,11 @@ public override int Execute() // Get project paths from the command line arguments PathUtility.EnsureAllPathsExist(_projects, CliStrings.CouldNotFindProjectOrDirectory, true); - IEnumerable fullProjectPaths = _projects.Select(project => + List fullProjectPaths = _projects.Select(project => { var fullPath = Path.GetFullPath(project); return Directory.Exists(fullPath) ? MsbuildProject.GetProjectFileFromDirectory(fullPath).FullName : fullPath; - }); + }).ToList(); // Check if we're working with a solution filter file if (_solutionFileFullPath.HasExtension(".slnf")) @@ -253,9 +253,10 @@ private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectP // Get solution-relative paths for new projects var newProjects = new List(); + string parentSolutionDirectory = Path.GetDirectoryName(parentSolutionPath) ?? string.Empty; foreach (var projectPath in projectPaths) { - string parentSolutionRelativePath = Path.GetRelativePath(Path.GetDirectoryName(parentSolutionPath)!, projectPath); + string parentSolutionRelativePath = Path.GetRelativePath(parentSolutionDirectory, projectPath); // Check if project exists in parent solution var projectInParent = parentSolution.FindProject(parentSolutionRelativePath); diff --git a/src/Cli/dotnet/SlnfFileHelper.cs b/src/Cli/dotnet/SlnfFileHelper.cs index ad19e1c5c7f1..c4a788bc87dc 100644 --- a/src/Cli/dotnet/SlnfFileHelper.cs +++ b/src/Cli/dotnet/SlnfFileHelper.cs @@ -39,7 +39,7 @@ private class SlnfRoot /// List of project paths to include (relative to the parent solution) public static void CreateSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects = null) { - var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)); + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)) ?? string.Empty; var parentSolutionFullPath = Path.GetFullPath(parentSolutionPath, slnfDirectory); var relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionFullPath); @@ -73,7 +73,7 @@ public static void CreateSolutionFilter(string slnfPath, string parentSolutionPa /// List of project paths (relative to the parent solution) public static void SaveSolutionFilter(string slnfPath, string parentSolutionPath, IEnumerable projects) { - var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)); + var slnfDirectory = Path.GetDirectoryName(Path.GetFullPath(slnfPath)) ?? string.Empty; // Normalize the parent solution path to be relative to the slnf file var relativeSolutionPath = parentSolutionPath; From 484dfcf8c8c2def71e64ff6c22227ce3aab97a4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:48:46 +0000 Subject: [PATCH 04/13] Add dotnetcli.host.json for slnf template and normalize path separators Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../dotnet/Commands/Solution/Add/SolutionAddCommand.cs | 5 ++++- src/Cli/dotnet/SlnFileFactory.cs | 2 ++ .../SolutionFilter/.template.config/dotnetcli.host.json | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index d846c9bf1f3b..04c781931598 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -248,7 +248,7 @@ private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectP // Load the parent solution to validate projects exist in it SolutionModel parentSolution = SlnFileFactory.CreateFromFileOrDirectory(parentSolutionPath); - // Get existing projects in the filter + // Get existing projects in the filter (already normalized to OS separator by CreateFromFilteredSolutionFile) var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); // Get solution-relative paths for new projects @@ -258,6 +258,9 @@ private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectP { string parentSolutionRelativePath = Path.GetRelativePath(parentSolutionDirectory, projectPath); + // Normalize to OS separator for consistent comparison + parentSolutionRelativePath = parentSolutionRelativePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + // Check if project exists in parent solution var projectInParent = parentSolution.FindProject(parentSolutionRelativePath); if (projectInParent is null) diff --git a/src/Cli/dotnet/SlnFileFactory.cs b/src/Cli/dotnet/SlnFileFactory.cs index 788752df3cff..6cbce88744b4 100644 --- a/src/Cli/dotnet/SlnFileFactory.cs +++ b/src/Cli/dotnet/SlnFileFactory.cs @@ -91,6 +91,8 @@ public static SolutionModel CreateFromFilteredSolutionFile(string filteredSoluti { JsonElement root = JsonDocument.Parse(File.ReadAllText(filteredSolutionPath)).RootElement; originalSolutionPath = Uri.UnescapeDataString(root.GetProperty("solution").GetProperty("path").GetString()); + // Normalize path separators to OS-specific for cross-platform compatibility + originalSolutionPath = originalSolutionPath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); filteredSolutionProjectPaths = [.. root.GetProperty("solution").GetProperty("projects").EnumerateArray().Select(p => p.GetString())]; originalSolutionPathAbsolute = Path.GetFullPath(originalSolutionPath, Path.GetDirectoryName(filteredSolutionPath)); } diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..6bc180f304f1 --- /dev/null +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/SolutionFilter/.template.config/dotnetcli.host.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "ParentSolution": { + "longName": "parent-solution", + "shortName": "s" + } + } +} From f2ad541c8a4bcfefc6003319c0ac84d695f703e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:10:04 +0000 Subject: [PATCH 05/13] Add tests for slnf template with --parent-solution and -s parameters Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../Solution-Filter-File/item.slnf | 6 ++++++ .../std-streams/stdout.txt | 1 + .../Solution-Filter-File/item.slnf | 6 ++++++ .../std-streams/stdout.txt | 1 + test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs | 3 +++ 5 files changed, 17 insertions(+) create mode 100644 test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf create mode 100644 test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt create mode 100644 test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf create mode 100644 test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf new file mode 100644 index 000000000000..e66e7dc8b150 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/Solution-Filter-File/item.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "Parent.slnx", + "projects": [] + } +} diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..70cab17a4b13 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#--parent-solution#Parent.slnx.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "%TEMPLATE_NAME%" was created successfully. \ No newline at end of file diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf new file mode 100644 index 000000000000..e66e7dc8b150 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/Solution-Filter-File/item.slnf @@ -0,0 +1,6 @@ +{ + "solution": { + "path": "Parent.slnx", + "projects": [] + } +} diff --git a/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt new file mode 100644 index 000000000000..70cab17a4b13 --- /dev/null +++ b/test/dotnet-new.IntegrationTests/Approvals/AllCommonItemsCreate.-o#Solution-Filter-File#-n#item#-s#Parent.slnx.verified/std-streams/stdout.txt @@ -0,0 +1 @@ +The template "%TEMPLATE_NAME%" was created successfully. \ No newline at end of file diff --git a/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs b/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs index ad2d5e54b48c..af33ea66d105 100644 --- a/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs +++ b/test/dotnet-new.IntegrationTests/CommonTemplatesTests.cs @@ -46,6 +46,9 @@ public CommonTemplatesTests(SharedHomeDirectory fixture, ITestOutputHelper log) [InlineData("Solution File", "sln", new[] { "--format", "sln" })] [InlineData("Solution File", "sln", new[] { "--format", "slnx" })] [InlineData("Solution File", "solution", null)] + [InlineData("Solution Filter File", "slnf", new[] { "--parent-solution", "Parent.slnx" })] + [InlineData("Solution Filter File", "slnf", new[] { "-s", "Parent.slnx" })] + [InlineData("Solution Filter File", "solutionfilter", new[] { "--parent-solution", "Parent.slnx" })] [InlineData("Dotnet local tool manifest file", "tool-manifest", null)] [InlineData("Web Config", "webconfig", null)] [InlineData("EditorConfig file", "editorconfig", null)] From 3c154af5a77e8f1ebcc0ad531c2713f9afdce042 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:26:58 +0000 Subject: [PATCH 06/13] Remove Solution1.slnf from Solution template and exclude it in template.json Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../content/Solution/.template.config/template.json | 4 ++-- .../content/Solution/Solution1.slnf | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json index 444f3b273938..3a865d9c0e5e 100644 --- a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json @@ -41,11 +41,11 @@ "modifiers": [ { "condition": "(Format == \"slnx\")", - "exclude": [ "Solution1.sln" ] + "exclude": [ "Solution1.sln", "Solution1.slnf" ] }, { "condition": "(Format == \"sln\")", - "exclude": [ "Solution1.slnx" ] + "exclude": [ "Solution1.slnx", "Solution1.slnf" ] } ] } diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf deleted file mode 100644 index 0578829ee51e..000000000000 --- a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/Solution1.slnf +++ /dev/null @@ -1,6 +0,0 @@ -{ - "solution": { - "path": "Solution1.slnx", - "projects": [] - } -} From 4f0f20a5554d54a319b8647f5b276348c1493619 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:39:25 +0000 Subject: [PATCH 07/13] Update CanDoTabCompletion test baseline to include slnf template Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt | 1 + .../DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt | 1 + ...otnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt index 58f8f94f7231..e8f07f91d9b2 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Linux.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt index 58f8f94f7231..e8f07f91d9b2 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.OSX.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports diff --git a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt index 6f90bdf0dd15..fdb015c97bef 100644 --- a/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt +++ b/test/dotnet-new.IntegrationTests/Approvals/DotnetNewCompleteTests.CanDoTabCompletion.Windows.verified.txt @@ -25,6 +25,7 @@ proto razorclasslib razorcomponent sln +slnf tool-manifest view viewimports From b3bc4e1810da5997a09d5c2f54533fb823b2aa77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 17:48:50 +0000 Subject: [PATCH 08/13] Revert unnecessary exclusion of Solution1.slnf from template.json Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- .../content/Solution/.template.config/template.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json index 3a865d9c0e5e..444f3b273938 100644 --- a/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json +++ b/template_feed/Microsoft.DotNet.Common.ItemTemplates/content/Solution/.template.config/template.json @@ -41,11 +41,11 @@ "modifiers": [ { "condition": "(Format == \"slnx\")", - "exclude": [ "Solution1.sln", "Solution1.slnf" ] + "exclude": [ "Solution1.sln" ] }, { "condition": "(Format == \"sln\")", - "exclude": [ "Solution1.slnx", "Solution1.slnf" ] + "exclude": [ "Solution1.slnx" ] } ] } From 94e1bf8f343be19ac0d5422b7e12d6fbca4bc573 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Oct 2025 18:52:27 +0000 Subject: [PATCH 09/13] Update tab completion test baselines for slnf template Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> --- ...abCompletionTests.Create_GetAllSuggestions.verified.txt | 7 +++++++ ...pletionTests.RootCommand_GetAllSuggestions.verified.txt | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt index 610ddc1db75b..c37686f6b955 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.Create_GetAllSuggestions.verified.txt @@ -118,6 +118,13 @@ InsertText: sln, Documentation: Create an empty solution containing no projects }, + { + Label: slnf, + Kind: Value, + SortText: slnf, + InsertText: slnf, + Documentation: Create a solution filter file that references a parent solution + }, { Label: tool-manifest, Kind: Value, diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt index b06accd6268a..313aef5edadb 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/ParserTests/Approvals/TabCompletionTests.RootCommand_GetAllSuggestions.verified.txt @@ -118,6 +118,13 @@ InsertText: sln, Documentation: Create an empty solution containing no projects }, + { + Label: slnf, + Kind: Value, + SortText: slnf, + InsertText: slnf, + Documentation: Create a solution filter file that references a parent solution + }, { Label: tool-manifest, Kind: Value, From 83a6e0ed211408ce5bbac1bb70b01337b8301ca9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 18:38:13 +0000 Subject: [PATCH 10/13] Fix duplicate using directives in ToolInstallGlobalOrToolPathCommand Co-authored-by: marcpopMSFT <12663534+marcpopMSFT@users.noreply.github.com> --- .../Tool/Install/ToolInstallGlobalOrToolPathCommand.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index 0224c1786f55..70d054d16da2 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -20,12 +20,6 @@ using NuGet.Frameworks; using NuGet.Versioning; using Microsoft.DotNet.Cli.CommandLine; -using Microsoft.DotNet.Cli.Extensions; -using Microsoft.DotNet.Cli.ShellShim; -using Microsoft.DotNet.Cli.Commands.Tool.Update; -using Microsoft.DotNet.Cli.Commands.Tool.Common; -using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; -using Microsoft.DotNet.Cli.Commands.Tool.List; namespace Microsoft.DotNet.Cli.Commands.Tool.Install; From 70be5c299d243107425ac8f9bfa9f12a54b11793 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:31:31 +0000 Subject: [PATCH 11/13] Address code review feedback: refactor slnf handling Co-authored-by: mthalman <15789599+mthalman@users.noreply.github.com> --- .../Solution/Add/SolutionAddCommand.cs | 32 ++++++++++++------ .../Solution/Remove/SolutionRemoveCommand.cs | 12 +++---- src/Cli/dotnet/SlnFileFactory.cs | 2 +- src/Cli/dotnet/SlnfFileHelper.cs | 33 ++++++++++++++++--- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index ef8d1e4dc053..6bea762448ce 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -65,9 +65,9 @@ public override int Execute() }).ToList(); // Check if we're working with a solution filter file - if (_solutionFileFullPath.HasExtension(".slnf")) + if (_solutionFileFullPath.HasExtension(SlnfFileHelper.SlnfExtension)) { - AddProjectsToSolutionFilterAsync(fullProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + AddProjectsToSolutionFilter(fullProjectPaths); } else { @@ -233,7 +233,7 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio } } - private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectPaths, CancellationToken cancellationToken) + private void AddProjectsToSolutionFilter(IEnumerable projectPaths) { // Solution filter files don't support --in-root or --solution-folder options if (_inRoot || !string.IsNullOrEmpty(_solutionFolderPath)) @@ -249,17 +249,33 @@ private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectP SolutionModel parentSolution = SlnFileFactory.CreateFromFileOrDirectory(parentSolutionPath); // Get existing projects in the filter (already normalized to OS separator by CreateFromFilteredSolutionFile) - var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + // Use case-insensitive comparer on Windows for file path comparison + var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(comparer); // Get solution-relative paths for new projects + var newProjects = ValidateAndGetNewProjects(projectPaths, parentSolution, parentSolutionPath, existingProjects); + + // Add new projects to the existing list and save + var allProjects = existingProjects.Concat(newProjects).OrderBy(p => p); + SlnfFileHelper.SaveSolutionFilter(_solutionFileFullPath, parentSolutionPath, allProjects); + } + + private List ValidateAndGetNewProjects( + IEnumerable projectPaths, + SolutionModel parentSolution, + string parentSolutionPath, + HashSet existingProjects) + { var newProjects = new List(); string parentSolutionDirectory = Path.GetDirectoryName(parentSolutionPath) ?? string.Empty; + foreach (var projectPath in projectPaths) { string parentSolutionRelativePath = Path.GetRelativePath(parentSolutionDirectory, projectPath); // Normalize to OS separator for consistent comparison - parentSolutionRelativePath = parentSolutionRelativePath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + parentSolutionRelativePath = SlnfFileHelper.NormalizePathSeparatorsToOS(parentSolutionRelativePath); // Check if project exists in parent solution var projectInParent = parentSolution.FindProject(parentSolutionRelativePath); @@ -280,10 +296,6 @@ private async Task AddProjectsToSolutionFilterAsync(IEnumerable projectP Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, parentSolutionRelativePath); } - // Add new projects to the existing list and save - var allProjects = existingProjects.Concat(newProjects).OrderBy(p => p); - SlnfFileHelper.SaveSolutionFilter(_solutionFileFullPath, parentSolutionPath, allProjects); - - await Task.CompletedTask; + return newProjects; } } diff --git a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs index dfe32f549a1c..26fdf297527f 100644 --- a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs @@ -44,9 +44,9 @@ public override int Execute() : p)); // Check if we're working with a solution filter file - if (solutionFileFullPath.HasExtension(".slnf")) + if (solutionFileFullPath.HasExtension(SlnfFileHelper.SlnfExtension)) { - RemoveProjectsFromSolutionFilterAsync(solutionFileFullPath, relativeProjectPaths, CancellationToken.None).GetAwaiter().GetResult(); + RemoveProjectsFromSolutionFilter(solutionFileFullPath, relativeProjectPaths); } else { @@ -139,14 +139,16 @@ private static async Task RemoveProjectsAsync(string solutionFileFullPath, IEnum await serializer.SaveAsync(solutionFileFullPath, solution, cancellationToken); } - private static async Task RemoveProjectsFromSolutionFilterAsync(string slnfFileFullPath, IEnumerable projectPaths, CancellationToken cancellationToken) + private static void RemoveProjectsFromSolutionFilter(string slnfFileFullPath, IEnumerable projectPaths) { // Load the filtered solution to get the parent solution path and existing projects SolutionModel filteredSolution = SlnFileFactory.CreateFromFilteredSolutionFile(slnfFileFullPath); string parentSolutionPath = filteredSolution.Description!; // The parent solution path is stored in Description // Get existing projects in the filter - var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); + // Use case-insensitive comparer on Windows for file path comparison + var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(comparer); // Remove specified projects foreach (var projectPath in projectPaths) @@ -167,7 +169,5 @@ private static async Task RemoveProjectsFromSolutionFilterAsync(string slnfFileF // Save updated filter SlnfFileHelper.SaveSolutionFilter(slnfFileFullPath, parentSolutionPath, existingProjects.OrderBy(p => p)); - - await Task.CompletedTask; } } diff --git a/src/Cli/dotnet/SlnFileFactory.cs b/src/Cli/dotnet/SlnFileFactory.cs index 6cbce88744b4..723195a66e36 100644 --- a/src/Cli/dotnet/SlnFileFactory.cs +++ b/src/Cli/dotnet/SlnFileFactory.cs @@ -92,7 +92,7 @@ public static SolutionModel CreateFromFilteredSolutionFile(string filteredSoluti JsonElement root = JsonDocument.Parse(File.ReadAllText(filteredSolutionPath)).RootElement; originalSolutionPath = Uri.UnescapeDataString(root.GetProperty("solution").GetProperty("path").GetString()); // Normalize path separators to OS-specific for cross-platform compatibility - originalSolutionPath = originalSolutionPath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + originalSolutionPath = SlnfFileHelper.NormalizePathSeparatorsToOS(originalSolutionPath); filteredSolutionProjectPaths = [.. root.GetProperty("solution").GetProperty("projects").EnumerateArray().Select(p => p.GetString())]; originalSolutionPathAbsolute = Path.GetFullPath(originalSolutionPath, Path.GetDirectoryName(filteredSolutionPath)); } diff --git a/src/Cli/dotnet/SlnfFileHelper.cs b/src/Cli/dotnet/SlnfFileHelper.cs index c4a788bc87dc..87d93b75d3d5 100644 --- a/src/Cli/dotnet/SlnfFileHelper.cs +++ b/src/Cli/dotnet/SlnfFileHelper.cs @@ -16,6 +16,31 @@ namespace Microsoft.DotNet.Cli; /// public static class SlnfFileHelper { + /// + /// File extension for solution filter files + /// + public const string SlnfExtension = ".slnf"; + + /// + /// Normalizes path separators from backslashes to the OS-specific directory separator + /// + /// The path to normalize + /// Path with OS-specific separators + public static string NormalizePathSeparatorsToOS(string path) + { + return path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + } + + /// + /// Normalizes path separators to backslashes (as used in .slnf files) + /// + /// The path to normalize + /// Path with backslash separators + public static string NormalizePathSeparatorsToBackslash(string path) + { + return path.Replace(Path.DirectorySeparatorChar, '\\'); + } + private class SlnfSolution { [JsonPropertyName("path")] @@ -44,14 +69,14 @@ public static void CreateSolutionFilter(string slnfPath, string parentSolutionPa var relativeSolutionPath = Path.GetRelativePath(slnfDirectory, parentSolutionFullPath); // Normalize path separators to backslashes (as per slnf format) - relativeSolutionPath = relativeSolutionPath.Replace(Path.DirectorySeparatorChar, '\\'); + relativeSolutionPath = NormalizePathSeparatorsToBackslash(relativeSolutionPath); var root = new SlnfRoot { Solution = new SlnfSolution { Path = relativeSolutionPath, - Projects = projects?.Select(p => p.Replace(Path.DirectorySeparatorChar, '\\')).ToList() ?? new List() + Projects = projects?.Select(NormalizePathSeparatorsToBackslash).ToList() ?? new List() } }; @@ -83,14 +108,14 @@ public static void SaveSolutionFilter(string slnfPath, string parentSolutionPath } // Normalize path separators to backslashes (as per slnf format) - relativeSolutionPath = relativeSolutionPath.Replace(Path.DirectorySeparatorChar, '\\'); + relativeSolutionPath = NormalizePathSeparatorsToBackslash(relativeSolutionPath); var root = new SlnfRoot { Solution = new SlnfSolution { Path = relativeSolutionPath, - Projects = projects.Select(p => p.Replace(Path.DirectorySeparatorChar, '\\')).ToList() + Projects = projects.Select(NormalizePathSeparatorsToBackslash).ToList() } }; From 75a51a8b9a190504fcef18c3b74166a7cca576cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:56:56 +0000 Subject: [PATCH 12/13] Revert case-insensitive comparer, use default ToHashSet() Co-authored-by: mthalman <15789599+mthalman@users.noreply.github.com> --- src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs | 4 +--- .../dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs index 6bea762448ce..800924d5c9ae 100644 --- a/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs @@ -249,9 +249,7 @@ private void AddProjectsToSolutionFilter(IEnumerable projectPaths) SolutionModel parentSolution = SlnFileFactory.CreateFromFileOrDirectory(parentSolutionPath); // Get existing projects in the filter (already normalized to OS separator by CreateFromFilteredSolutionFile) - // Use case-insensitive comparer on Windows for file path comparison - var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(comparer); + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); // Get solution-relative paths for new projects var newProjects = ValidateAndGetNewProjects(projectPaths, parentSolution, parentSolutionPath, existingProjects); diff --git a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs index 26fdf297527f..f8a26281632f 100644 --- a/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs +++ b/src/Cli/dotnet/Commands/Solution/Remove/SolutionRemoveCommand.cs @@ -147,8 +147,7 @@ private static void RemoveProjectsFromSolutionFilter(string slnfFileFullPath, IE // Get existing projects in the filter // Use case-insensitive comparer on Windows for file path comparison - var comparer = OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(comparer); + var existingProjects = filteredSolution.SolutionProjects.Select(p => p.FilePath).ToHashSet(); // Remove specified projects foreach (var projectPath in projectPaths) From 0e6f777f8d1cac7071997d4346942c3ec91d0acc Mon Sep 17 00:00:00 2001 From: Marc Paine Date: Thu, 8 Jan 2026 17:02:16 -0800 Subject: [PATCH 13/13] Fix bad merge --- src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs b/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs index 82a98f402492..04f59da98594 100644 --- a/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs +++ b/src/Cli/dotnet/Commands/Package/Search/PackageSearchCommand.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.CommandLine; using Microsoft.DotNet.Cli.Commands.NuGet; using Microsoft.DotNet.Cli.CommandLine; using Microsoft.DotNet.Cli.Extensions;