Skip to content
Closed
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
28 changes: 6 additions & 22 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -487,30 +487,14 @@ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(

private async Task<int> CreateEmptyAppHostAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
ITemplate template;

if (_features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
{
// Use single-file AppHost template if feature is enabled
var singleFileTemplate = _templateFactory.GetAllTemplates().FirstOrDefault(t => t.Name == "aspire-apphost-singlefile");
if (singleFileTemplate is null)
{
InteractionService.DisplayError("Single-file AppHost template not found.");
return ExitCodeConstants.FailedToCreateNewProject;
}
template = singleFileTemplate;
}
else
// Use single-file AppHost template
var singleFileTemplate = _templateFactory.GetAllTemplates().FirstOrDefault(t => t.Name == "aspire-apphost-singlefile");
if (singleFileTemplate is null)
{
// Use regular AppHost template if single-file feature is not enabled
var appHostTemplate = _templateFactory.GetAllTemplates().FirstOrDefault(t => t.Name == "aspire-apphost");
if (appHostTemplate is null)
{
InteractionService.DisplayError("AppHost template not found.");
return ExitCodeConstants.FailedToCreateNewProject;
}
template = appHostTemplate;
InteractionService.DisplayError("Single-file AppHost template not found.");
return ExitCodeConstants.FailedToCreateNewProject;
}
var template = singleFileTemplate;

var result = await template.ApplyTemplateAsync(parseResult, cancellationToken);

Expand Down
9 changes: 0 additions & 9 deletions src/Aspire.Cli/Commands/PublishCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,6 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

var isSingleFileAppHost = effectiveAppHostFile.Extension != ".csproj";

// Validate that single file AppHost feature is enabled if we detected a .cs file
if (isSingleFileAppHost && !_features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
{
// Send terminal progress bar stop sequence
StopTerminalProgressBar();
InteractionService.DisplayError(ErrorStrings.SingleFileAppHostFeatureNotEnabled);
return ExitCodeConstants.FailedToFindProject;
}

var env = new Dictionary<string, string>();

// Set interactivity enabled based on host environment capabilities
Expand Down
7 changes: 0 additions & 7 deletions src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,6 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

var isSingleFileAppHost = effectiveAppHostFile.Extension != ".csproj";

// Validate that single file AppHost feature is enabled if we detected a .cs file
if (isSingleFileAppHost && !_features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
{
InteractionService.DisplayError(ErrorStrings.SingleFileAppHostFeatureNotEnabled);
return ExitCodeConstants.FailedToFindProject;
}

var env = new Dictionary<string, string>();

var debug = parseResult.GetValue<bool>("--debug");
Expand Down
16 changes: 3 additions & 13 deletions src/Aspire.Cli/DotNet/DotNetSdkInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ internal sealed class DotNetSdkInstaller(IFeatures features, IConfiguration conf
/// <summary>
/// The minimum .NET SDK version required for Aspire.
/// </summary>
public const string MinimumSdkVersion = "9.0.302";

/// <summary>
/// The minimum .NET SDK version required for Aspire when .NET 10 features are enabled.
/// </summary>
public const string MinimumSdkNet10SdkVersion = "10.0.100";
public const string MinimumSdkVersion = "10.0.100";

/// <inheritdoc />
public async Task<(bool Success, string? HighestVersion, string MinimumRequiredVersion)> CheckAsync(CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -131,7 +126,7 @@ private static string GetCurrentArchitecture()
}

/// <summary>
/// Gets the effective minimum SDK version based on configuration and feature flags.
/// Gets the effective minimum SDK version based on configuration.
/// </summary>
/// <returns>The minimum SDK version string.</returns>
public string GetEffectiveMinimumSdkVersion()
Expand All @@ -143,11 +138,6 @@ public string GetEffectiveMinimumSdkVersion()
{
return overrideVersion;
}
else if (features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false) ||
features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, false))
{
return MinimumSdkNet10SdkVersion;
}
else
{
return MinimumSdkVersion;
Expand All @@ -165,7 +155,7 @@ public string GetEffectiveMinimumSdkVersion()
private static bool MeetsMinimumRequirement(SemVersion installedVersion, SemVersion requiredVersion, string requiredVersionString)
{
// Special handling for .NET 10.0.100 requirement - allow any .NET 10.x version
if (requiredVersionString == MinimumSdkNet10SdkVersion)
if (requiredVersionString == MinimumSdkVersion)
{
// If we require 10.0.100, accept any version that is >= 10.0.0
return installedVersion.Major >= 10;
Expand Down
1 change: 0 additions & 1 deletion src/Aspire.Cli/KnownFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ internal static class KnownFeatures
public static string ExecCommandEnabled => "execCommandEnabled";
public static string OrphanDetectionWithTimestampEnabled => "orphanDetectionWithTimestampEnabled";
public static string ShowDeprecatedPackages => "showDeprecatedPackages";
public static string SingleFileAppHostEnabled => "singlefileAppHostEnabled";
public static string PackageSearchDiskCachingEnabled => "packageSearchDiskCachingEnabled";
public static string StagingChannelEnabled => "stagingChannelEnabled";
public static string DefaultWatchEnabled => "defaultWatchEnabled";
Expand Down
48 changes: 17 additions & 31 deletions src/Aspire.Cli/Projects/ProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal interface IProjectLocator
Task<IReadOnlyList<FileInfo>> FindExecutableProjectsAsync(string searchDirectory, CancellationToken cancellationToken);
}

internal sealed class ProjectLocator(ILogger<ProjectLocator> logger, IDotNetCliRunner runner, CliExecutionContext executionContext, IInteractionService interactionService, IConfigurationService configurationService, AspireCliTelemetry telemetry, IFeatures features) : IProjectLocator
internal sealed class ProjectLocator(ILogger<ProjectLocator> logger, IDotNetCliRunner runner, CliExecutionContext executionContext, IInteractionService interactionService, IConfigurationService configurationService, AspireCliTelemetry telemetry) : IProjectLocator
{
public async Task<List<FileInfo>> FindAppHostProjectFilesAsync(string searchDirectory, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -83,21 +83,19 @@ await Parallel.ForEachAsync(projectFiles, parallelOptions, async (projectFile, c
}
});

// Scan for single-file apphosts (new logic)
if (features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
{
logger.LogDebug("Searching for single-file apphosts in {SearchDirectory}", searchDirectory.FullName);
var candidateAppHostFiles = searchDirectory.GetFiles("apphost.cs", enumerationOptions);
logger.LogDebug("Found {CandidateFileCount} single-file apphost candidates in {SearchDirectory}", candidateAppHostFiles.Length, searchDirectory.FullName);
// Scan for single-file apphosts
logger.LogDebug("Searching for single-file apphosts in {SearchDirectory}", searchDirectory.FullName);
var candidateAppHostFiles = searchDirectory.GetFiles("apphost.cs", enumerationOptions);
logger.LogDebug("Found {CandidateFileCount} single-file apphost candidates in {SearchDirectory}", candidateAppHostFiles.Length, searchDirectory.FullName);

await Parallel.ForEachAsync(candidateAppHostFiles, parallelOptions, async (candidateFile, ct) =>
await Parallel.ForEachAsync(candidateAppHostFiles, parallelOptions, async (candidateFile, ct) =>
{
logger.LogDebug("Checking single-file apphost candidate {CandidateFile}", candidateFile.FullName);

if (await IsValidSingleFileAppHostAsync(candidateFile, ct))
{
logger.LogDebug("Checking single-file apphost candidate {CandidateFile}", candidateFile.FullName);

if (await IsValidSingleFileAppHostAsync(candidateFile, ct))
{
logger.LogDebug("Found single-file apphost candidate {CandidateFile} in {SearchDirectory}", candidateFile.FullName, searchDirectory.FullName);
var relativePath = Path.GetRelativePath(executionContext.WorkingDirectory.FullName, candidateFile.FullName);
logger.LogDebug("Found single-file apphost candidate {CandidateFile} in {SearchDirectory}", candidateFile.FullName, searchDirectory.FullName);
var relativePath = Path.GetRelativePath(executionContext.WorkingDirectory.FullName, candidateFile.FullName);
interactionService.DisplaySubtleMessage(relativePath);
lock (lockObject)
{
Expand All @@ -109,11 +107,6 @@ await Parallel.ForEachAsync(candidateAppHostFiles, parallelOptions, async (candi
logger.LogTrace("Single-file candidate {CandidateFile} in {SearchDirectory} is not a valid apphost", candidateFile.FullName, searchDirectory.FullName);
}
});
}
else
{
logger.LogTrace("Single-file apphost feature is disabled, skipping single-file apphost discovery");
}

// This sort is done here to make results deterministic since we get all the app
// host information in parallel and the order may vary.
Expand Down Expand Up @@ -265,8 +258,8 @@ await Parallel.ForEachAsync(allProjectFiles, parallelOptions, async (candidatePr
}
});

// If no .csproj AppHost files found and single-file apphost is enabled, check for apphost.cs
if (foundProjects.Count == 0 && features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
// If no .csproj AppHost files found, check for apphost.cs
if (foundProjects.Count == 0)
{
var appHostFiles = directory.GetFiles("apphost.cs", enumerationOptions);
logger.LogDebug("Found {CandidateFileCount} single-file apphost candidates", appHostFiles.Length);
Expand Down Expand Up @@ -328,17 +321,10 @@ await Parallel.ForEachAsync(appHostFiles, parallelOptions, async (candidateFile,
// Handle explicit apphost.cs files
if (projectFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase))
{
if (features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
if (await IsValidSingleFileAppHostAsync(projectFile, cancellationToken))
{
if (await IsValidSingleFileAppHostAsync(projectFile, cancellationToken))
{
logger.LogDebug("Using single-file apphost {ProjectFile}", projectFile.FullName);
return projectFile;
}
else
{
throw new ProjectLocatorException(ErrorStrings.ProjectFileDoesntExist);
}
logger.LogDebug("Using single-file apphost {ProjectFile}", projectFile.FullName);
return projectFile;
}
else
{
Expand Down
34 changes: 15 additions & 19 deletions src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,22 @@ private IEnumerable<ITemplate> GetTemplatesCore(bool showAllTemplates)
(template, parseResult, ct) => ApplyTemplateAsync(template, parseResult, PromptForExtraAspireStarterOptionsAsync, ct)
);

// Single-file AppHost template (gated by feature flag). This template only exists in the pack
// and should be surfaced to the user when the single-file AppHost feature is enabled.
if (features.IsFeatureEnabled(KnownFeatures.SingleFileAppHostEnabled, false))
{
yield return new CallbackTemplate(
"aspire-py-starter",
TemplatingStrings.AspirePyStarter_Description,
projectName => $"./{projectName}",
_ => { },
ApplySingleFileTemplate
);
// Single-file AppHost templates
yield return new CallbackTemplate(
"aspire-py-starter",
TemplatingStrings.AspirePyStarter_Description,
projectName => $"./{projectName}",
_ => { },
ApplySingleFileTemplate
);

yield return new CallbackTemplate(
"aspire-apphost-singlefile",
TemplatingStrings.AspireAppHostSingleFile_Description,
projectName => $"./{projectName}",
_ => { },
ApplySingleFileTemplate
);
}
yield return new CallbackTemplate(
"aspire-apphost-singlefile",
TemplatingStrings.AspireAppHostSingleFile_Description,
projectName => $"./{projectName}",
_ => { },
ApplySingleFileTemplate
);

if (showAllTemplates)
{
Expand Down
3 changes: 0 additions & 3 deletions tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,6 @@ public async Task InitCommand_WithSingleFileAppHost_DoesNotPromptForProjectNameO

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
// Enable single-file AppHost feature
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];

// Set up prompter to track if prompts are called
options.NewCommandPrompterFactory = (sp) =>
{
Expand Down
20 changes: 1 addition & 19 deletions tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -497,24 +497,6 @@ public async Task RunCommand_SkipsBuild_WhenExtensionDevKitCapabilityIsAvailable
Assert.False(buildCalled, "Build should be skipped when extension DevKit capability is available.");
}

[Fact]
public async Task RunCommand_WhenSingleFileAppHostAndFeatureDisabled_ReturnsNonZeroExitCode()
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.ProjectLocatorFactory = _ => new SingleFileAppHostProjectLocator();
// Feature is disabled by default in tests, so we don't need to explicitly disable it
});
var provider = services.BuildServiceProvider();

var command = provider.GetRequiredService<RootCommand>();
var result = command.Parse("run");

var exitCode = await result.InvokeAsync().WaitAsync(CliTestConstants.DefaultTimeout);
Assert.Equal(ExitCodeConstants.FailedToFindProject, exitCode);
}

[Fact]
public async Task RunCommand_WhenSingleFileAppHostAndDefaultWatchEnabled_DoesNotUseWatchMode()
{
Expand Down Expand Up @@ -561,7 +543,7 @@ public async Task RunCommand_WhenSingleFileAppHostAndDefaultWatchEnabled_DoesNot
options.ProjectLocatorFactory = _ => new SingleFileAppHostProjectLocator();
options.AppHostBackchannelFactory = backchannelFactory;
options.DotNetCliRunnerFactory = runnerFactory;
options.EnabledFeatures = [KnownFeatures.DefaultWatchEnabled, KnownFeatures.SingleFileAppHostEnabled];
options.EnabledFeatures = [KnownFeatures.DefaultWatchEnabled];
});

var provider = services.BuildServiceProvider();
Expand Down
20 changes: 4 additions & 16 deletions tests/Aspire.Cli.Tests/DotNet/DotNetCliRunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,7 @@ public async Task AddPackageAsyncUseFilesSwitchForSingleFileAppHost()
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
Expand Down Expand Up @@ -874,10 +871,7 @@ public async Task RunAsyncAppliesNoLaunchProfileForSingleFileAppHost()
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
Expand Down Expand Up @@ -932,10 +926,7 @@ public async Task RunAsyncDoesNotIncludeNoLaunchProfileForSingleFileAppHostWhenN
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
Expand Down Expand Up @@ -1043,10 +1034,7 @@ public async Task RunAsyncFiltersOutEmptyArgumentsForSingleFileAppHost()
var appHostFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.cs"));
await File.WriteAllTextAsync(appHostFile.FullName, "// Single-file AppHost");

var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
{
options.EnabledFeatures = [KnownFeatures.SingleFileAppHostEnabled];
});
var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper);
var provider = services.BuildServiceProvider();
var logger = provider.GetRequiredService<ILogger<DotNetCliRunner>>();
var interactionService = provider.GetRequiredService<IInteractionService>();
Expand Down
Loading
Loading