diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index 614b7fb7653..86581aa6f14 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -175,6 +175,25 @@ private async Task InitializeExistingSolutionAsync(InitContext initContext, { var solutionFile = initContext.SelectedSolutionFile!; + // Verify that the solution directory does not contain project files. + // If the solution and a project file are in the same directory, the AppHost + // and ServiceDefaults directories would be created inside that project which + // is not supported. + var solutionDirectory = solutionFile.Directory!; + var projectFileInSolutionDir = solutionDirectory.EnumerateFiles() + .FirstOrDefault(f => DotNetAppHostProject.ProjectExtensions.Contains(f.Extension, StringComparer.OrdinalIgnoreCase)); + + if (projectFileInSolutionDir is not null) + { + InteractionService.DisplayError( + string.Format( + CultureInfo.CurrentCulture, + InitCommandStrings.SolutionAndProjectInSameDirectory, + solutionFile.Name, + projectFileInSolutionDir.Name)); + return ExitCodeConstants.FailedToCreateNewProject; + } + initContext.GetSolutionProjectsOutputCollector = new OutputCollector(); var (getSolutionExitCode, solutionProjects) = await InteractionService.ShowStatusAsync("Reading solution...", async () => { diff --git a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs index af3b9f6a92f..b3c70ce2081 100644 --- a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs +++ b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs @@ -34,7 +34,8 @@ internal sealed class DotNetAppHostProject : IAppHostProject private readonly Diagnostics.FileLoggerProvider _fileLoggerProvider; private static readonly string[] s_detectionPatterns = ["*.csproj", "*.fsproj", "*.vbproj", "apphost.cs"]; - private static readonly string[] s_projectExtensions = [".csproj", ".fsproj", ".vbproj"]; + internal static IReadOnlyCollection ProjectExtensions { get; } = + Array.AsReadOnly([".csproj", ".fsproj", ".vbproj"]); public DotNetAppHostProject( IDotNetCliRunner runner, @@ -85,7 +86,7 @@ public bool CanHandle(FileInfo appHostFile) var extension = appHostFile.Extension.ToLowerInvariant(); // Handle project files (.csproj, .fsproj, .vbproj) - if (s_projectExtensions.Contains(extension)) + if (ProjectExtensions.Contains(extension)) { // We can handle any project file - ValidateAsync will do deeper validation return true; @@ -520,7 +521,7 @@ public async Task FindAndStopRunningInstanceAsync(FileInf // Auto-initialize user secrets (only for csproj projects - file-based apphosts // always have a UserSecretsId provided by the SDK) - if (!s_projectExtensions.Contains(projectFile.Extension.ToLowerInvariant())) + if (!ProjectExtensions.Contains(projectFile.Extension.ToLowerInvariant())) { return userSecretsId; } diff --git a/src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs index 9e80ff89783..676950e46d9 100644 --- a/src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/InitCommandStrings.Designer.cs @@ -116,5 +116,11 @@ internal static string ResolvingTemplateVersion { return ResourceManager.GetString("ResolvingTemplateVersion", resourceCulture); } } + + internal static string SolutionAndProjectInSameDirectory { + get { + return ResourceManager.GetString("SolutionAndProjectInSameDirectory", resourceCulture); + } + } } } \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/InitCommandStrings.resx b/src/Aspire.Cli/Resources/InitCommandStrings.resx index dc351b5cdea..7d56a851605 100644 --- a/src/Aspire.Cli/Resources/InitCommandStrings.resx +++ b/src/Aspire.Cli/Resources/InitCommandStrings.resx @@ -96,4 +96,7 @@ Resolving template version... + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.cs.xlf index 77985587fa2..8ae0e9c9df8 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.cs.xlf @@ -62,6 +62,11 @@ Řešení se už inicializovalo pomocí Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.de.xlf index fd2a2dcb6cc..767e6237565 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.de.xlf @@ -62,6 +62,11 @@ Die Lösung wurde bereits mit Aspire initialisiert. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.es.xlf index 1903b29a032..b6ed5d391ed 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.es.xlf @@ -62,6 +62,11 @@ La solución ya está inicializada con Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.fr.xlf index 6c44497d7b6..8101a7735d4 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.fr.xlf @@ -62,6 +62,11 @@ La solution a déjà été initialisée avec Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.it.xlf index 92ffe6e9570..4457c9abdae 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.it.xlf @@ -62,6 +62,11 @@ La soluzione è già stata inizializzata con Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ja.xlf index 3acbcc62965..a968a2da8c7 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ja.xlf @@ -62,6 +62,11 @@ ソリューションは既に Aspire で初期化されています。 + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ko.xlf index 0c729e510ec..2490d2a0128 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ko.xlf @@ -62,6 +62,11 @@ 솔루션이 이미 Aspire로 초기화되었습니다. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pl.xlf index bc20d2a4897..c78303f19f8 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pl.xlf @@ -62,6 +62,11 @@ Rozwiązanie zostało już zainicjowane za pomocą narzędzia Prześlij. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pt-BR.xlf index 6a1855c9316..6454571eaa7 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.pt-BR.xlf @@ -62,6 +62,11 @@ A solução já foi inicializada com o Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ru.xlf index dc12fdf76cf..d641ad48b9a 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.ru.xlf @@ -62,6 +62,11 @@ Решение уже инициализировано с помощью Aspire. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.tr.xlf index 97a32f836c1..a4ba3e381ad 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.tr.xlf @@ -62,6 +62,11 @@ Çözüm zaten Aspire ile başlatıldı. + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hans.xlf index f3257b8e8b7..aa2bf214d15 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hans.xlf @@ -62,6 +62,11 @@ 解决方案已使用 Aspire 初始化。 + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hant.xlf index ad62f82e797..74b78591ba1 100644 --- a/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/InitCommandStrings.zh-Hant.xlf @@ -62,6 +62,11 @@ 解決方案已隨著 Aspire 初始化。 + + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + The solution file '{0}' and project file '{1}' are in the same directory. The AppHost and ServiceDefaults projects cannot be created inside an existing project directory. Move the solution file to a parent directory or the project file to a subdirectory. + + \ No newline at end of file diff --git a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs index 985ffa8bf4b..e60465309a7 100644 --- a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs @@ -14,6 +14,103 @@ namespace Aspire.Cli.Tests.Commands; public class InitCommandTests(ITestOutputHelper outputHelper) { + [Theory] + [InlineData("Test.csproj")] + [InlineData("Test.fsproj")] + [InlineData("Test.vbproj")] + public async Task InitCommand_WhenSolutionAndProjectInSameDirectory_ReturnsError(string projectFileName) + { + // Arrange + using var workspace = TemporaryWorkspace.Create(outputHelper); + + // Create a solution file and a project file in the same directory + var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln")); + File.WriteAllText(solutionFile.FullName, "Fake solution file"); + + var projectFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, projectFileName)); + File.WriteAllText(projectFile.FullName, ""); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + // GetSolutionProjectsAsync should not be called because the check + // happens before reading solution projects + runner.GetSolutionProjectsAsyncCallback = (_, _, _) => + { + throw new InvalidOperationException("GetSolutionProjectsAsync should not be called when solution and project are in the same directory."); + }; + return runner; + }; + }); + + var serviceProvider = services.BuildServiceProvider(); + var initCommand = serviceProvider.GetRequiredService(); + + // Act + var parseResult = initCommand.Parse("init"); + var exitCode = await parseResult.InvokeAsync().DefaultTimeout(); + + // Assert + Assert.Equal(ExitCodeConstants.FailedToCreateNewProject, exitCode); + } + + [Fact] + public async Task InitCommand_WhenSolutionDirectoryHasNoProjectFiles_Proceeds() + { + // Arrange + using var workspace = TemporaryWorkspace.Create(outputHelper); + + // Create a solution file only (no project files in the same directory) + var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln")); + File.WriteAllText(solutionFile.FullName, "Fake solution file"); + + var getSolutionProjectsCalled = false; + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.GetSolutionProjectsAsyncCallback = (_, _, _) => + { + getSolutionProjectsCalled = true; + // Return success with no projects - the test verifies the check passed + return (0, Array.Empty()); + }; + runner.NewProjectAsyncCallback = (_, _, outputPath, _, _) => + { + // Create the expected directories so the code can find them + var appHostDir = Path.Combine(outputPath, "Test.AppHost"); + var serviceDefaultsDir = Path.Combine(outputPath, "Test.ServiceDefaults"); + Directory.CreateDirectory(appHostDir); + Directory.CreateDirectory(serviceDefaultsDir); + File.WriteAllText(Path.Combine(appHostDir, "Test.AppHost.csproj"), ""); + File.WriteAllText(Path.Combine(serviceDefaultsDir, "Test.ServiceDefaults.csproj"), ""); + return 0; + }; + return runner; + }; + options.PackagingServiceFactory = (sp) => + { + return new TestPackagingService(); + }; + }); + + var serviceProvider = services.BuildServiceProvider(); + var initCommand = serviceProvider.GetRequiredService(); + + // Act + var parseResult = initCommand.Parse("init"); + var exitCode = await parseResult.InvokeAsync().DefaultTimeout(); + + // Assert - the command should have proceeded past the directory check and created projects + Assert.True(getSolutionProjectsCalled, "GetSolutionProjectsAsync should have been called when no project files are in the solution directory."); + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.AppHost", "Test.AppHost.csproj"))); + Assert.True(File.Exists(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.ServiceDefaults", "Test.ServiceDefaults.csproj"))); + } + [Fact] public void InitContext_RequiredAppHostFramework_ReturnsHighestTfm() {