diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 8763b850..44fea504 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -167,10 +167,22 @@ We support } } + bool evaluate = true; + + val = configurationSection[nameof(FeatureDefinition.Evaluate)]; + + if (!string.IsNullOrEmpty(val) && !bool.TryParse(val, out evaluate)) + { + throw new FeatureManagementException( + FeatureManagementError.InvalidConfiguration, + $"The feature '{configurationSection.Key}' has an invalid value for the '{nameof(FeatureDefinition.Evaluate)}' property."); + } + return new FeatureDefinition() { Name = configurationSection.Key, - EnabledFor = enabledFor + EnabledFor = enabledFor, + Evaluate = evaluate }; } diff --git a/src/Microsoft.FeatureManagement/FeatureDefinition.cs b/src/Microsoft.FeatureManagement/FeatureDefinition.cs index 4931fd5b..de02d2fb 100644 --- a/src/Microsoft.FeatureManagement/FeatureDefinition.cs +++ b/src/Microsoft.FeatureManagement/FeatureDefinition.cs @@ -19,5 +19,11 @@ public class FeatureDefinition /// The feature filters that the feature can be enabled for. /// public IEnumerable EnabledFor { get; set; } = new List(); + + /// + /// Dictates whether the feature should be evaluated. If false, the feature will be considered disabled. + /// The default value is true. + /// + public bool Evaluate { get; set; } = true; } } diff --git a/src/Microsoft.FeatureManagement/FeatureManagementError.cs b/src/Microsoft.FeatureManagement/FeatureManagementError.cs index 1cdb9eed..8b3f84bd 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagementError.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagementError.cs @@ -21,6 +21,11 @@ public enum FeatureManagementError /// /// A feature that was requested for evaluation was not found. /// - MissingFeature + MissingFeature, + + /// + /// An invalid configuration was encountered when performing a feature management operation. + /// + InvalidConfiguration } } diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index 097af3a2..4aad0a52 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -74,7 +74,8 @@ private async Task IsEnabledAsync(string feature, TContext appCo FeatureDefinition featureDefinition = await _featureDefinitionProvider.GetFeatureDefinitionAsync(feature).ConfigureAwait(false); - if (featureDefinition != null) + if (featureDefinition != null + && featureDefinition.Evaluate) { // // Check if feature is always on @@ -141,7 +142,7 @@ private async Task IsEnabledAsync(string feature, TContext appCo } } } - else + else if (featureDefinition == null) { string errorMessage = $"The feature declaration for the feature '{feature}' was not found."; diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 2dade89c..a0caa6b9 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -26,6 +26,8 @@ public class FeatureManagement private const string OffFeature = "OffFeature"; private const string ConditionalFeature = "ConditionalFeature"; private const string ContextualFeature = "ContextualFeature"; + private const string AlwaysOnFeature = "AlwaysOnFeature"; + private const string NonEvaluatedAlwaysOnFeature = "NonEvaluatedAlwaysOnFeature"; [Fact] public async Task ReadsConfiguration() @@ -615,6 +617,26 @@ public async Task ThreadsafeSnapshot() } } + [Fact] + public async Task TogglesEvaluation() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + var services = new ServiceCollection(); + + services + .AddSingleton(config) + .AddFeatureManagement(); + + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IFeatureManager featureManager = serviceProvider.GetRequiredService(); + + Assert.True(await featureManager.IsEnabledAsync(AlwaysOnFeature)); + + Assert.False(await featureManager.IsEnabledAsync(NonEvaluatedAlwaysOnFeature)); + } + private static void DisableEndpointRouting(MvcOptions options) { #if NET5_0 || NETCOREAPP3_1 diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index f033a400..0bfad5d4 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -63,6 +63,21 @@ } } ] + }, + "AlwaysOnFeature": { + "EnabledFor": [ + { + "Name": "AlwaysOn" + } + ] + }, + "NonEvaluatedAlwaysOnFeature": { + "Evaluate": false, + "EnabledFor": [ + { + "Name": "AlwaysOn" + } + ] } } }