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;