Skip to content

sln-add: --include-references when adding a project #48815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2483,4 +2483,7 @@ To display a value, specify the corresponding command-line option without provid
<data name="ZeroTestsRan" xml:space="preserve">
<value>Zero tests ran</value>
</data>
</root>
<data name="SolutionAddReferencedProjectsOptionDescription" xml:space="preserve">
<value>Recursively add projects' ReferencedProjects to solution</value>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referring to ReferencedProjects is clear to you but probably opaque to most people

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Recursively add project references to the solution"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about "Set to false to disable recursively adding all ProjectReferences"? My initial thought was that ReferencedProjects is an internal implementation detail, but looking now, it'd also be nice to include that the default is 'true'.

</data>
</root>
21 changes: 19 additions & 2 deletions src/Cli/dotnet/Commands/Solution/Add/SolutionAddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal class SolutionAddCommand : CommandBase
private readonly IReadOnlyCollection<string> _projects;
private readonly string? _solutionFolderPath;
private string _solutionFileFullPath = string.Empty;
private bool _includeReferences;

private static string GetSolutionFolderPathWithForwardSlashes(string path)
{
Expand All @@ -43,6 +44,7 @@ public SolutionAddCommand(ParseResult parseResult) : base(parseResult)
_projects = (IReadOnlyCollection<string>)(parseResult.GetValue(SolutionAddCommandParser.ProjectPathArgument) ?? []);
_inRoot = parseResult.GetValue(SolutionAddCommandParser.InRootOption);
_solutionFolderPath = parseResult.GetValue(SolutionAddCommandParser.SolutionFolderOption);
_includeReferences = parseResult.GetValue(SolutionAddCommandParser.IncludeReferencesOption);
SolutionArgumentValidator.ParseAndValidateArguments(_fileOrDirectory, _projects, SolutionArgumentValidator.CommandType.Add, _inRoot, _solutionFolderPath);
_solutionFileFullPath = SlnFileFactory.GetSolutionFileFullPath(_fileOrDirectory);
}
Expand Down Expand Up @@ -138,7 +140,7 @@ private async Task AddProjectsToSolutionAsync(IEnumerable<string> projectPaths,
await serializer.SaveAsync(_solutionFileFullPath, solution, cancellationToken);
}

private void AddProject(SolutionModel solution, string fullProjectPath, ISolutionSerializer serializer = null)
private void AddProject(SolutionModel solution, string fullProjectPath, ISolutionSerializer serializer = null, bool showMessageOnDuplicate = true)
{
string solutionRelativeProjectPath = Path.GetRelativePath(Path.GetDirectoryName(_solutionFileFullPath), fullProjectPath);

Expand Down Expand Up @@ -175,7 +177,10 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio
}
catch (SolutionArgumentException ex) when (ex.Type == SolutionErrorType.DuplicateProjectName || solution.FindProject(solutionRelativeProjectPath) is not null)
{
Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, _solutionFileFullPath, solutionRelativeProjectPath);
if (showMessageOnDuplicate)
{
Reporter.Output.WriteLine(CliStrings.SolutionAlreadyContainsProject, _solutionFileFullPath, solutionRelativeProjectPath);
}
return;
}

Expand Down Expand Up @@ -205,5 +210,17 @@ private void AddProject(SolutionModel solution, string fullProjectPath, ISolutio
}

Reporter.Output.WriteLine(CliStrings.ProjectAddedToTheSolution, solutionRelativeProjectPath);

// Get referencedprojects from the project instance
var referencedProjectsFullPaths = projectInstance.GetItems("ProjectReference")
.Select(item => Path.GetFullPath(item.EvaluatedInclude, Path.GetDirectoryName(fullProjectPath)));

if (_includeReferences)
Copy link
Preview

Copilot AI May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding cycle detection or duplicate addition checks when recursively adding referenced projects to prevent potential infinite recursion in cases of cyclic project references.

Copilot uses AI. Check for mistakes.

{
foreach (var referencedProjectFullPath in referencedProjectsFullPaths)
{
AddProject(solution, referencedProjectFullPath, serializer, showMessageOnDuplicate: false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#nullable disable

using System.CommandLine;
using System.CommandLine.Parsing;

namespace Microsoft.DotNet.Cli.Commands.Solution.Add;

Expand All @@ -26,6 +27,12 @@ public static class SolutionAddCommandParser
Description = CliCommandStrings.AddProjectSolutionFolderArgumentDescription
};

public static readonly Option<bool> IncludeReferencesOption = new("--include-references")
{
Description = CliCommandStrings.SolutionAddReferencedProjectsOptionDescription,
DefaultValueFactory = (_) => true,
};

private static readonly Command Command = ConstructCommand();

public static Command GetCommand()
Expand All @@ -40,6 +47,7 @@ private static Command ConstructCommand()
command.Arguments.Add(ProjectPathArgument);
command.Options.Add(InRootOption);
command.Options.Add(SolutionFolderOption);
command.Options.Add(IncludeReferencesOption);

command.SetAction((parseResult) => new SolutionAddCommand(parseResult).Execute());

Expand Down
5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\B\B.csproj" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions test/TestAssets/TestProjects/SlnFileWithReferencedProjects/App.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<Solution>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
38 changes: 38 additions & 0 deletions test/dotnet.Tests/CommandTests/Solution/Add/GivenDotnetSlnAdd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.VisualStudio.SolutionPersistence;
using Microsoft.VisualStudio.SolutionPersistence.Model;
using Microsoft.DotNet.Cli.Commands;
using System.Threading.Tasks;

namespace Microsoft.DotNet.Cli.Sln.Add.Tests
{
Expand Down Expand Up @@ -36,6 +37,7 @@ dotnet solution <SLN_FILE> add [<PROJECT_PATH>...] [options]
Options:
--in-root Place project in root of the solution, rather than creating a solution folder.
-s, --solution-folder <solution-folder> The destination solution folder path to add the projects to.
--include-references Recursively add projects' ReferencedProjects to solution [default: True]
-?, -h, --help Show command line help";

public GivenDotnetSlnAdd(ITestOutputHelper log) : base(log)
Expand Down Expand Up @@ -1154,6 +1156,40 @@ public async Task WhenAddingProjectOutsideDirectoryItShouldNotAddSolutionFolders
solution.SolutionFolders.Count.Should().Be(0);
}

[Theory]
[InlineData("sln", ".sln", "--include-references=true")]
[InlineData("solution", ".sln", "--include-references=true")]
[InlineData("sln", ".slnx", "--include-references=true")]
[InlineData("solution", ".slnx", "--include-references=true")]
[InlineData("sln", ".sln", "--include-references=false")]
[InlineData("solution", ".sln", "--include-references=false")]
[InlineData("sln", ".slnx", "--include-references=false")]
[InlineData("solution", ".slnx", "--include-references=false")]
public async Task WhenSolutionIsPassedAProjectWithReferenceItAddsOtherProjectUnlessSpecified(string solutionCommand, string solutionExtension, string option)
{
var projectDirectory = _testAssetsManager
.CopyTestAsset("SlnFileWithReferencedProjects", identifier: $"GivenDotnetSlnAdd-{solutionCommand}")
.WithSource()
.Path;
var projectToAdd = Path.Combine("A", "A.csproj");
var cmd = new DotnetCommand(Log)
.WithWorkingDirectory(Path.Join(projectDirectory))
.Execute(solutionCommand, $"App{solutionExtension}", "add", projectToAdd, option);
cmd.Should().Pass();
// Should have two projects
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(Path.Join(projectDirectory, $"App{solutionExtension}"));
SolutionModel solution = await serializer.OpenAsync(Path.Join(projectDirectory, $"App{solutionExtension}"), CancellationToken.None);

if (option.Equals("--include-references=false")) // Option is true by default
{
solution.SolutionProjects.Count.Should().Be(1);
}
else
{
solution.SolutionProjects.Count.Should().Be(2);
}
}

private string GetExpectedSlnContents(
string slnPath,
string slnTemplateName,
Expand Down Expand Up @@ -1237,6 +1273,8 @@ public void WhenSolutionIsPassedAsProjectWithSolutionFolderItPrintsSuggestionAnd
{
VerifySuggestionAndUsage(solutionCommand, "--solution-folder", solutionExtension);
}


private void VerifySuggestionAndUsage(string solutionCommand, string arguments, string solutionExtension)
{
var projectDirectory = _testAssetsManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1421,7 +1421,7 @@ _testhost_solution_add() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=()

opts="--in-root --solution-folder --help"
opts="--in-root --solution-folder --include-references --help"

if [[ $COMP_CWORD == "$1" ]]; then
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
Expand All @@ -1433,6 +1433,10 @@ _testhost_solution_add() {
COMPREPLY=( $(compgen -W "False True" -- "$cur") )
return
;;
--include-references)
COMPREPLY=( $(compgen -W "False True" -- "$cur") )
return
;;
esac

COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock {
[CompletionResult]::new('--in-root', '--in-root', [CompletionResultType]::ParameterName, "Place project in root of the solution, rather than creating a solution folder.")
[CompletionResult]::new('--solution-folder', '--solution-folder', [CompletionResultType]::ParameterName, "The destination solution folder path to add the projects to.")
[CompletionResult]::new('--solution-folder', '-s', [CompletionResultType]::ParameterName, "The destination solution folder path to add the projects to.")
[CompletionResult]::new('--include-references', '--include-references', [CompletionResultType]::ParameterName, "Recursively add projects`' ReferencedProjects to solution")
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, "Show command line help.")
[CompletionResult]::new('--help', '-h', [CompletionResultType]::ParameterName, "Show command line help.")
)
Expand Down
Loading