diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs index 80f540e8..073f953f 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs @@ -4,73 +4,33 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; namespace Microsoft.FeatureManagement { /// - /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature (or set of features) is enabled. + /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature is enabled. /// /// The filter that will be used instead of this placeholder. class FeatureGatedAsyncActionFilter : IAsyncActionFilter where T : IAsyncActionFilter { - /// - /// Creates a feature gated filter for multiple features with a specified requirement type and ability to negate the evaluation. - /// - /// Specifies whether all or any of the provided features should be enabled. - /// Whether to negate the evaluation result. - /// The features that control whether the wrapped filter executes. - public FeatureGatedAsyncActionFilter(RequirementType requirementType, bool negate, params string[] features) + public FeatureGatedAsyncActionFilter(string featureName) { - if (features == null || features.Length == 0) + if (string.IsNullOrEmpty(featureName)) { - throw new ArgumentNullException(nameof(features)); + throw new ArgumentNullException(nameof(featureName)); } - Features = features; - RequirementType = requirementType; - Negate = negate; + FeatureName = featureName; } - /// - /// The set of features that gate the wrapped filter. - /// - public IEnumerable Features { get; } - - /// - /// Controls whether any or all features in should be enabled to allow the wrapped filter to execute. - /// - public RequirementType RequirementType { get; } - - /// - /// Negates the evaluation for whether or not the wrapped filter should execute. - /// - public bool Negate { get; } + public string FeatureName { get; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - IFeatureManagerSnapshot featureManager = context.HttpContext.RequestServices.GetRequiredService(); - - bool enabled; - - // Enabled state is determined by either 'any' or 'all' features being enabled. - if (RequirementType == RequirementType.All) - { - enabled = await Features.All(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false)); - } - else - { - enabled = await Features.Any(async f => await featureManager.IsEnabledAsync(f).ConfigureAwait(false)); - } - - if (Negate) - { - enabled = !enabled; - } + IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService(); - if (enabled) + if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false)) { IAsyncActionFilter filter = ActivatorUtilities.CreateInstance(context.HttpContext.RequestServices); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index 86bece85..cfd46554 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,63 +16,12 @@ public static class FilterCollectionExtensions /// The MVC filter to add and use if the feature is enabled. /// The filter collection to add to. /// The feature that will need to enabled to trigger the execution of the MVC filter. - /// The reference to the added filter metadata. + /// public static IFilterMetadata AddForFeature(this FilterCollection filters, string feature) where TFilterType : IAsyncActionFilter { - IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, feature); + IFilterMetadata filterMetadata = null; - filters.Add(filterMetadata); - - return filterMetadata; - } - - /// - /// Adds an MVC filter that will only activate during a request if the specified feature is enabled. - /// - /// The MVC filter to add and use if the feature is enabled. - /// The filter collection to add to. - /// The features that control whether the MVC filter executes. - /// The reference to the added filter metadata. - public static IFilterMetadata AddForFeature(this FilterCollection filters, params string[] features) where TFilterType : IAsyncActionFilter - { - IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, features); - - filters.Add(filterMetadata); - - return filterMetadata; - } - - /// - /// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type. - /// - /// The MVC filter to add and use if the features condition is satisfied. - /// The filter collection to add to. - /// Specifies whether all or any of the provided features should be enabled. - /// The features that control whether the MVC filter executes. - /// The reference to the added filter metadata. - public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, params string[] features) where TFilterType : IAsyncActionFilter - { - IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, false, features); - - filters.Add(filterMetadata); - - return filterMetadata; - } - - /// - /// Adds an MVC filter that will only activate during a request if the specified features are enabled based on the provided requirement type and negation flag. - /// - /// The MVC filter to add and use if the features condition is satisfied. - /// The filter collection to add to. - /// Specifies whether all or any of the provided features should be enabled. - /// Whether to negate the evaluation result for the provided features set. - /// The features that control whether the MVC filter executes. - /// The reference to the added filter metadata. - public static IFilterMetadata AddForFeature(this FilterCollection filters, RequirementType requirementType, bool negate, params string[] features) where TFilterType : IAsyncActionFilter - { - IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(requirementType, negate, features); - - filters.Add(filterMetadata); + filters.Add(new FeatureGatedAsyncActionFilter(feature)); return filterMetadata; } diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 42b212cb..12aa4e1f 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -5,7 +5,7 @@ 4 - 4 + 3 0 diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj index 97dbb7ff..d195cfa8 100644 --- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj +++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj @@ -4,7 +4,7 @@ 4 - 4 + 3 0 diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 389d129f..62b5f4a5 100644 --- a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs +++ b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs @@ -37,9 +37,9 @@ public TimeWindowFilter(ILoggerFactory loggerFactory = null) public IMemoryCache Cache { get; set; } /// - /// This property allows the time window filter to use custom . + /// This property allows the time window filter in our test suite to use simulated time. /// - public TimeProvider SystemClock { get; set; } + internal TimeProvider SystemClock { get; set; } /// /// Binds configuration representing filter parameters to . diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 3c043152..12004aea 100644 --- a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs +++ b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs @@ -103,7 +103,7 @@ public async ValueTask GetVariantAsync(string feature, CancellationToke // // First, check local cache - if (_variantCache.ContainsKey(cacheKey)) + if (_variantCache.ContainsKey(feature)) { return _variantCache[cacheKey]; } @@ -121,7 +121,7 @@ public async ValueTask GetVariantAsync(string feature, ITargetingContex // // First, check local cache - if (_variantCache.ContainsKey(cacheKey)) + if (_variantCache.ContainsKey(feature)) { return _variantCache[cacheKey]; } diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index e815b031..560a6c07 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -5,7 +5,7 @@ 4 - 4 + 3 0 diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index db43e8c6..806c66fa 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -175,8 +175,7 @@ private static IFeatureManagementBuilder GetFeatureManagementBuilder(IServiceCol builder.AddFeatureFilter(sp => new TimeWindowFilter() { - Cache = sp.GetRequiredService(), - SystemClock = sp.GetService() ?? TimeProvider.System, + Cache = sp.GetRequiredService() }); builder.AddFeatureFilter(); diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs index 8bfd5ec4..68b7efc1 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs +++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs @@ -202,47 +202,6 @@ public async Task GatesRazorPageFeatures() Assert.Equal(HttpStatusCode.OK, gateAnyNegateResponse.StatusCode); } - [Fact] - public async Task GatesActionFilterFeatures() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - - TestServer server = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => - { - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); - - services.AddMvcCore(o => - { - DisableEndpointRouting(o); - o.Filters.AddForFeature(RequirementType.All, Features.ConditionalFeature, Features.ConditionalFeature2); - }); - }).Configure(app => app.UseMvc())); - - TestFilter filter = (TestFilter)server.Host.Services.GetRequiredService>().First(f => f is TestFilter); - HttpClient client = server.CreateClient(); - - // - // Enable all features - filter.Callback = _ => Task.FromResult(true); - HttpResponseMessage res = await client.GetAsync(""); - Assert.True(res.Headers.Contains(nameof(MvcFilter))); - - // - // Enable 1/2 features - filter.Callback = ctx => Task.FromResult(ctx.FeatureName == Features.ConditionalFeature); - res = await client.GetAsync(""); - Assert.False(res.Headers.Contains(nameof(MvcFilter))); - - // - // Enable no - filter.Callback = _ => Task.FromResult(false); - res = await client.GetAsync(""); - Assert.False(res.Headers.Contains(nameof(MvcFilter))); - } - private static void DisableEndpointRouting(MvcOptions options) { options.EnableEndpointRouting = false; diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 10636b84..1b8ae34b 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -332,67 +332,6 @@ public async Task ThreadSafeSnapshot() } } - [Fact] - public async Task ReturnsCachedResultFromSnapshot() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - - var services = new ServiceCollection(); - - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); - - ServiceProvider serviceProvider = services.BuildServiceProvider(); - - IVariantFeatureManager featureManager = serviceProvider.GetRequiredService(); - - IVariantFeatureManager featureManagerSnapshot = serviceProvider.GetRequiredService(); - - IEnumerable featureFilters = serviceProvider.GetRequiredService>(); - - TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); - - int callCount = 0; - bool filterEnabled = true; - - testFeatureFilter.Callback = (evaluationContext) => - { - callCount++; - return Task.FromResult(filterEnabled); - }; - - // First evaluation - filter is enabled and should return true - bool result1 = await featureManagerSnapshot.IsEnabledAsync(Features.ConditionalFeature); - Assert.Equal(1, callCount); - Assert.True(result1); - - Variant variant1 = await featureManagerSnapshot.GetVariantAsync(Features.ConditionalFeature); - Assert.Equal(2, callCount); - Assert.Equal("DefaultWhenEnabled", variant1.Name); - - // "Shut down" the feature filter - filterEnabled = false; - - // Second evaluation - should use cached value despite filter being shut down - bool result2 = await featureManagerSnapshot.IsEnabledAsync(Features.ConditionalFeature); - Assert.Equal(2, callCount); - Assert.True(result2); - - Variant variant2 = await featureManagerSnapshot.GetVariantAsync(Features.ConditionalFeature); - Assert.Equal(2, callCount); - Assert.Equal("DefaultWhenEnabled", variant2.Name); - - bool result3 = await featureManager.IsEnabledAsync(Features.ConditionalFeature); - Assert.Equal(3, callCount); - Assert.False(result3); - - Variant variant3 = await featureManager.GetVariantAsync(Features.ConditionalFeature); - Assert.Equal(4, callCount); - Assert.Equal("DefaultWhenDisabled", variant3.Name); - } - [Fact] public void AddsScopedFeatureManagement() { @@ -584,20 +523,6 @@ public async Task MergesFeatureFlagsFromDifferentConfigurationSources() Assert.True(await featureManager8.IsEnabledAsync("FeatureC")); Assert.False(await featureManager8.IsEnabledAsync("Feature1")); Assert.False(await featureManager8.IsEnabledAsync("Feature2")); - - var configurationManager = new ConfigurationManager(); - configurationManager - .AddJsonFile("appsettings1.json") - .AddJsonFile("appsettings2.json"); - - var services = new ServiceCollection(); - services.AddFeatureManagement(); - - var featureManager9 = new FeatureManager(new ConfigurationFeatureDefinitionProvider(configurationManager, mergeOptions)); - Assert.True(await featureManager9.IsEnabledAsync("FeatureA")); - Assert.True(await featureManager9.IsEnabledAsync("FeatureB")); - Assert.True(await featureManager9.IsEnabledAsync("Feature1")); - Assert.False(await featureManager9.IsEnabledAsync("Feature2")); // appsettings2 should override appsettings1 } } diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 018ef5c4..e192a5ff 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -28,18 +28,6 @@ } } ] - }, - "variants": [ - { - "name": "DefaultWhenEnabled" - }, - { - "name": "DefaultWhenDisabled" - } - ], - "allocation": { - "default_when_enabled": "DefaultWhenEnabled", - "default_when_disabled": "DefaultWhenDisabled" } }, {