Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU

var exitCode = await ExecuteAsync(parseResult, cancellationToken);

if (UpdateNotificationsEnabled && features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, true))
if (UpdateNotificationsEnabled && features.Enabled<UpdateNotificationsEnabledFeature>())
{
try
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/RootCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public RootCommand(
Subcommands.Add(extensionInternalCommand);
Subcommands.Add(mcpCommand);

if (featureFlags.IsFeatureEnabled(KnownFeatures.ExecCommandEnabled, false))
if (featureFlags.Enabled<ExecCommandEnabledFeature>())
{
Subcommands.Add(execCommand);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell

await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken);

var watch = !isSingleFileAppHost && (_features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false) || (isExtensionHost && !startDebugSession));
var watch = !isSingleFileAppHost && (_features.Enabled<DefaultWatchEnabledFeature>() || (isExtensionHost && !startDebugSession));

if (!watch)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public UpdateCommand(
Options.Add(selfOption);

// Customize description based on whether staging channel is enabled
var isStagingEnabled = _features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false);
var isStagingEnabled = _features.Enabled<StagingChannelEnabledFeature>();

var channelOption = new Option<string?>("--channel")
{
Expand Down
94 changes: 94 additions & 0 deletions src/Aspire.Cli/Configuration/FeatureFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Cli.Configuration;

/// <summary>
/// Feature flag for enabling update notifications.
/// </summary>
internal sealed class UpdateNotificationsEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "updateNotificationsEnabled";
public bool DefaultValue => true;
}

/// <summary>
/// Feature flag for enabling minimum SDK version checking.
/// </summary>
internal sealed class MinimumSdkCheckEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "minimumSdkCheckEnabled";
public bool DefaultValue => true;
}

/// <summary>
/// Feature flag for enabling the exec command.
/// </summary>
internal sealed class ExecCommandEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "execCommandEnabled";
public bool DefaultValue => false;
}

/// <summary>
/// Feature flag for enabling orphan detection with timestamp.
/// </summary>
internal sealed class OrphanDetectionWithTimestampEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "orphanDetectionWithTimestampEnabled";
public bool DefaultValue => true;
}

/// <summary>
/// Feature flag for showing deprecated packages.
/// </summary>
internal sealed class ShowDeprecatedPackagesFeature : IFeatureFlag
{
public string ConfigurationKey => "showDeprecatedPackages";
public bool DefaultValue => false;
}

/// <summary>
/// Feature flag for enabling package search disk caching.
/// </summary>
internal sealed class PackageSearchDiskCachingEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "packageSearchDiskCachingEnabled";
public bool DefaultValue => true;
}

/// <summary>
/// Feature flag for enabling the staging channel.
/// </summary>
internal sealed class StagingChannelEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "stagingChannelEnabled";
public bool DefaultValue => false;
}

/// <summary>
/// Feature flag for enabling watch mode by default.
/// </summary>
internal sealed class DefaultWatchEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "defaultWatchEnabled";
public bool DefaultValue => false;
}

/// <summary>
/// Feature flag for showing all templates.
/// </summary>
internal sealed class ShowAllTemplatesFeature : IFeatureFlag
{
public string ConfigurationKey => "showAllTemplates";
public bool DefaultValue => false;
}

/// <summary>
/// Feature flag for enabling .NET SDK installation.
/// </summary>
internal sealed class DotNetSdkInstallationEnabledFeature : IFeatureFlag
{
public string ConfigurationKey => "dotnetSdkInstallationEnabled";
public bool DefaultValue => true;
}
21 changes: 20 additions & 1 deletion src/Aspire.Cli/Configuration/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ namespace Aspire.Cli.Configuration;

internal sealed class Features(IConfiguration configuration) : IFeatures
{
private static readonly Dictionary<Type, IFeatureFlag> s_featureFlagCache = new();

public bool IsFeatureEnabled(string feature, bool defaultValue)
{
var configKey = $"features:{feature}";
Expand All @@ -18,6 +20,23 @@ public bool IsFeatureEnabled(string feature, bool defaultValue)
return defaultValue;
}

return bool.TryParse(value, out var enabled) && enabled;
if (bool.TryParse(value, out var enabled))
{
return enabled;
}

return defaultValue;
}

public bool Enabled<TFeatureFlag>() where TFeatureFlag : IFeatureFlag, new()
{
// Cache the feature flag instance to avoid repeated allocations
if (!s_featureFlagCache.TryGetValue(typeof(TFeatureFlag), out var featureFlag))
{
featureFlag = new TFeatureFlag();
s_featureFlagCache[typeof(TFeatureFlag)] = featureFlag;
}

return IsFeatureEnabled(featureFlag.ConfigurationKey, featureFlag.DefaultValue);
}
}
20 changes: 20 additions & 0 deletions src/Aspire.Cli/Configuration/IFeatureFlag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Cli.Configuration;

/// <summary>
/// Represents a feature flag with its configuration key and default value.
/// </summary>
internal interface IFeatureFlag
{
/// <summary>
/// Gets the configuration key used to look up the feature flag value.
/// </summary>
string ConfigurationKey { get; }

/// <summary>
/// Gets the default value for the feature flag when not explicitly configured.
/// </summary>
bool DefaultValue { get; }
}
7 changes: 7 additions & 0 deletions src/Aspire.Cli/Configuration/IFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,11 @@ namespace Aspire.Cli.Configuration;
internal interface IFeatures
{
bool IsFeatureEnabled(string featureFlag, bool defaultValue);

/// <summary>
/// Checks if a feature flag is enabled using a type-safe feature flag definition.
/// </summary>
/// <typeparam name="TFeatureFlag">The type of feature flag to check.</typeparam>
/// <returns>True if the feature is enabled, false otherwise.</returns>
bool Enabled<TFeatureFlag>() where TFeatureFlag : IFeatureFlag, new();
}
8 changes: 4 additions & 4 deletions src/Aspire.Cli/DotNet/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild,
}

// Check if update notifications are disabled and set version check environment variable
if (!features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, defaultValue: true))
if (!features.Enabled<UpdateNotificationsEnabledFeature>())
{
// Copy the environment if we haven't already
if (finalEnv == env)
Expand Down Expand Up @@ -300,7 +300,7 @@ public async Task<int> RunAsync(FileInfo projectFile, bool watch, bool noBuild,
}
}

if (features.IsFeatureEnabled(KnownFeatures.DotNetSdkInstallationEnabled, true))
if (features.Enabled<DotNetSdkInstallationEnabledFeature>())
{
if (finalEnv == env)
{
Expand Down Expand Up @@ -551,7 +551,7 @@ public virtual async Task<int> ExecuteAsync(string[] args, IDictionary<string, s

// Set the CLI process start time for robust orphan detection to prevent PID reuse issues.
// The AppHost will verify both PID and start time to ensure it's monitoring the correct process.
if (features.IsFeatureEnabled(KnownFeatures.OrphanDetectionWithTimestampEnabled, true))
if (features.Enabled<OrphanDetectionWithTimestampEnabledFeature>())
{
startInfo.EnvironmentVariables[KnownConfigNames.CliProcessStarted] = GetCurrentProcessStartTime().ToString(CultureInfo.InvariantCulture);
}
Expand Down Expand Up @@ -928,7 +928,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
using var activity = telemetry.ActivitySource.StartActivity();

string? rawKey = null;
bool cacheEnabled = useCache && features.IsFeatureEnabled(KnownFeatures.PackageSearchDiskCachingEnabled, defaultValue: true);
bool cacheEnabled = useCache && features.Enabled<PackageSearchDiskCachingEnabledFeature>();
if (cacheEnabled)
{
try
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/DotNet/DotNetSdkInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal sealed class DotNetSdkInstaller(IFeatures features, IConfiguration conf
}
}

if (!features.IsFeatureEnabled(KnownFeatures.MinimumSdkCheckEnabled, true))
if (!features.Enabled<MinimumSdkCheckEnabledFeature>())
{
// If the feature is disabled, we assume the SDK is available
return (true, null, minimumVersion, forceInstall);
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/NuGet/NuGetPackageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public async Task<IEnumerable<NuGetPackage>> GetPackagesAsync(DirectoryInfo work
var isOfficialPackage = IsOfficialOrCommunityToolkitPackage(p.Id);

// Apply deprecated package filter unless the user wants to show deprecated packages
if (isOfficialPackage && !features.IsFeatureEnabled(KnownFeatures.ShowDeprecatedPackages, defaultValue: false))
if (isOfficialPackage && !features.Enabled<ShowDeprecatedPackagesFeature>())
{
return !s_deprecatedPackages.Contains(p.Id);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/NuGet/NuGetPackagePrefetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_ = Task.Run(async () =>
{
if (features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, true))
if (features.Enabled<UpdateNotificationsEnabledFeature>())
{
try
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Packaging/PackagingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc
var channels = new List<PackageChannel>([defaultChannel, stableChannel]);

// Add staging channel if feature is enabled (after stable, before daily)
if (features.IsFeatureEnabled(KnownFeatures.StagingChannelEnabled, false))
if (features.Enabled<StagingChannelEnabledFeature>())
{
var stagingChannel = CreateStagingChannel();
if (stagingChannel is not null)
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ internal class DotNetTemplateFactory(
{
public IEnumerable<ITemplate> GetTemplates()
{
var showAllTemplates = features.IsFeatureEnabled(KnownFeatures.ShowAllTemplates, false);
var showAllTemplates = features.Enabled<ShowAllTemplatesFeature>();
return GetTemplatesCore(showAllTemplates);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Utils/SdkInstallHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ public static async Task<bool> EnsureSdkInstalledAsync(
}

// Only offer to install if:
// 1. The feature is enabled (default: false)
// 1. The feature is enabled
// 2. We support interactive input OR forceInstall is true (for testing)
if (features.IsFeatureEnabled(KnownFeatures.DotNetSdkInstallationEnabled, defaultValue: false) &&
if (features.Enabled<DotNetSdkInstallationEnabledFeature>() &&
(hostEnvironment?.SupportsInteractiveInput == true || forceInstall))
{
bool shouldInstall;
Expand Down
Loading
Loading