From 74ea9e955c54f82bb0f3c1db1877bff0d3fd8dfa Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:52:14 +0800 Subject: [PATCH 1/7] Merge pull request #556 from microsoft/zhiyuanliang/fix-snapshot-bug Fix snapshot cache key bug --- .../FeatureManagerSnapshot.cs | 4 +- .../FeatureManagementTest.cs | 61 +++++++++++++++++++ .../Tests.FeatureManagement/appsettings.json | 12 ++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs b/src/Microsoft.FeatureManagement/FeatureManagerSnapshot.cs index 12004aea..3c043152 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(feature)) + if (_variantCache.ContainsKey(cacheKey)) { return _variantCache[cacheKey]; } @@ -121,7 +121,7 @@ public async ValueTask GetVariantAsync(string feature, ITargetingContex // // First, check local cache - if (_variantCache.ContainsKey(feature)) + if (_variantCache.ContainsKey(cacheKey)) { return _variantCache[cacheKey]; } diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 1b8ae34b..32201d93 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -332,6 +332,67 @@ 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() { diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index e192a5ff..018ef5c4 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -28,6 +28,18 @@ } } ] + }, + "variants": [ + { + "name": "DefaultWhenEnabled" + }, + { + "name": "DefaultWhenDisabled" + } + ], + "allocation": { + "default_when_enabled": "DefaultWhenEnabled", + "default_when_disabled": "DefaultWhenDisabled" } }, { From 2ce8f5f1c928d90c4739f50b3466bcd57923308e Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:18:27 +0800 Subject: [PATCH 2/7] add testcase for configuration manager (#557) --- .../FeatureManagementTest.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index 32201d93..10636b84 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -584,6 +584,20 @@ 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 } } From 7b4f387193a004608a64582eb1400e1adb0c7978 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:08:48 +0800 Subject: [PATCH 3/7] Expose time provider for TimeWindowFilter (#562) --- .../FeatureFilters/TimeWindowFilter.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 62b5f4a5..389d129f 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 in our test suite to use simulated time. + /// This property allows the time window filter to use custom . /// - internal TimeProvider SystemClock { get; set; } + public TimeProvider SystemClock { get; set; } /// /// Binds configuration representing filter parameters to . diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 806c66fa..db43e8c6 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -175,7 +175,8 @@ private static IFeatureManagementBuilder GetFeatureManagementBuilder(IServiceCol builder.AddFeatureFilter(sp => new TimeWindowFilter() { - Cache = sp.GetRequiredService() + Cache = sp.GetRequiredService(), + SystemClock = sp.GetService() ?? TimeProvider.System, }); builder.AddFeatureFilter(); From d8061e9b4eccf80eeeeea77af2e2fe40319f0783 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:23:56 +0800 Subject: [PATCH 4/7] Update FilterCollectionExtensions (#359) * update * update * update * update AddForFeature * add test * update --- .../FeatureGatedAsyncActionFilter.cs | 56 +++++++++++++++--- .../FilterCollectionExtensions.cs | 57 ++++++++++++++++++- .../FeatureManagementAspNetCore.cs | 41 +++++++++++++ 3 files changed, 143 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs index 073f953f..80f540e8 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs @@ -4,33 +4,73 @@ 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 is enabled. + /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature (or set of features) is enabled. /// /// The filter that will be used instead of this placeholder. class FeatureGatedAsyncActionFilter : IAsyncActionFilter where T : IAsyncActionFilter { - public FeatureGatedAsyncActionFilter(string featureName) + /// + /// 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) { - if (string.IsNullOrEmpty(featureName)) + if (features == null || features.Length == 0) { - throw new ArgumentNullException(nameof(featureName)); + throw new ArgumentNullException(nameof(features)); } - FeatureName = featureName; + Features = features; + RequirementType = requirementType; + Negate = negate; } - public string FeatureName { get; } + /// + /// 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 async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService(); + 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; + } - if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false)) + if (enabled) { IAsyncActionFilter filter = ActivatorUtilities.CreateInstance(context.HttpContext.RequestServices); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index cfd46554..86bece85 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,12 +16,63 @@ 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 = null; + IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, feature); - filters.Add(new FeatureGatedAsyncActionFilter(feature)); + 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); return filterMetadata; } diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs index 68b7efc1..8bfd5ec4 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs +++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs @@ -202,6 +202,47 @@ 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; From 804dbd807726334827bc354faa48ee02912c0154 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:09:34 +0800 Subject: [PATCH 5/7] version bump 4.4.0 (#569) --- .../Microsoft.FeatureManagement.AspNetCore.csproj | 2 +- ...osoft.FeatureManagement.Telemetry.ApplicationInsights.csproj | 2 +- .../Microsoft.FeatureManagement.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 12aa4e1f..42b212cb 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -5,7 +5,7 @@ 4 - 3 + 4 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 d195cfa8..97dbb7ff 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 - 3 + 4 0 diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 560a6c07..e815b031 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -5,7 +5,7 @@ 4 - 3 + 4 0 From a74d56b8a5d2459760e8a24dce39edf5eabf690c Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:48:40 +0800 Subject: [PATCH 6/7] Merge main to release/v4 (#570) * Merge pull request #556 from microsoft/zhiyuanliang/fix-snapshot-bug Fix snapshot cache key bug * add testcase for configuration manager (#557) * Expose time provider for TimeWindowFilter (#562) * Update FilterCollectionExtensions (#359) * update * update * update * update AddForFeature * add test * update * version bump 4.4.0 (#569) --- .../FeatureGatedAsyncActionFilter.cs | 56 ++++++++++++-- .../FilterCollectionExtensions.cs | 57 +++++++++++++- ...rosoft.FeatureManagement.AspNetCore.csproj | 2 +- ...ement.Telemetry.ApplicationInsights.csproj | 2 +- .../FeatureFilters/TimeWindowFilter.cs | 4 +- .../FeatureManagerSnapshot.cs | 4 +- .../Microsoft.FeatureManagement.csproj | 2 +- .../ServiceCollectionExtensions.cs | 3 +- .../FeatureManagementAspNetCore.cs | 41 ++++++++++ .../FeatureManagementTest.cs | 75 +++++++++++++++++++ .../Tests.FeatureManagement/appsettings.json | 12 +++ 11 files changed, 239 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs index 073f953f..80f540e8 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FeatureGatedAsyncActionFilter.cs @@ -4,33 +4,73 @@ 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 is enabled. + /// A place holder MVC filter that is used to dynamically activate a filter based on whether a feature (or set of features) is enabled. /// /// The filter that will be used instead of this placeholder. class FeatureGatedAsyncActionFilter : IAsyncActionFilter where T : IAsyncActionFilter { - public FeatureGatedAsyncActionFilter(string featureName) + /// + /// 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) { - if (string.IsNullOrEmpty(featureName)) + if (features == null || features.Length == 0) { - throw new ArgumentNullException(nameof(featureName)); + throw new ArgumentNullException(nameof(features)); } - FeatureName = featureName; + Features = features; + RequirementType = requirementType; + Negate = negate; } - public string FeatureName { get; } + /// + /// 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 async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { - IFeatureManager featureManager = context.HttpContext.RequestServices.GetRequiredService(); + 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; + } - if (await featureManager.IsEnabledAsync(FeatureName).ConfigureAwait(false)) + if (enabled) { IAsyncActionFilter filter = ActivatorUtilities.CreateInstance(context.HttpContext.RequestServices); diff --git a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs index cfd46554..86bece85 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/FilterCollectionExtensions.cs @@ -16,12 +16,63 @@ 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 = null; + IFilterMetadata filterMetadata = new FeatureGatedAsyncActionFilter(RequirementType.Any, false, feature); - filters.Add(new FeatureGatedAsyncActionFilter(feature)); + 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); return filterMetadata; } diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 12aa4e1f..42b212cb 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -5,7 +5,7 @@ 4 - 3 + 4 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 d195cfa8..97dbb7ff 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 - 3 + 4 0 diff --git a/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs b/src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs index 62b5f4a5..389d129f 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 in our test suite to use simulated time. + /// This property allows the time window filter to use custom . /// - internal TimeProvider SystemClock { get; set; } + public 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 12004aea..3c043152 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(feature)) + if (_variantCache.ContainsKey(cacheKey)) { return _variantCache[cacheKey]; } @@ -121,7 +121,7 @@ public async ValueTask GetVariantAsync(string feature, ITargetingContex // // First, check local cache - if (_variantCache.ContainsKey(feature)) + if (_variantCache.ContainsKey(cacheKey)) { return _variantCache[cacheKey]; } diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index 560a6c07..e815b031 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -5,7 +5,7 @@ 4 - 3 + 4 0 diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 806c66fa..db43e8c6 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -175,7 +175,8 @@ private static IFeatureManagementBuilder GetFeatureManagementBuilder(IServiceCol builder.AddFeatureFilter(sp => new TimeWindowFilter() { - Cache = sp.GetRequiredService() + Cache = sp.GetRequiredService(), + SystemClock = sp.GetService() ?? TimeProvider.System, }); builder.AddFeatureFilter(); diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs index 68b7efc1..8bfd5ec4 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs +++ b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs @@ -202,6 +202,47 @@ 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 1b8ae34b..10636b84 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -332,6 +332,67 @@ 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() { @@ -523,6 +584,20 @@ 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 e192a5ff..018ef5c4 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -28,6 +28,18 @@ } } ] + }, + "variants": [ + { + "name": "DefaultWhenEnabled" + }, + { + "name": "DefaultWhenDisabled" + } + ], + "allocation": { + "default_when_enabled": "DefaultWhenEnabled", + "default_when_disabled": "DefaultWhenDisabled" } }, { From 206a208d2fee7281ca4f6c4d0551c3088cb02a39 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:56:28 +0800 Subject: [PATCH 7/7] Revert "Merge main to release/v4 (#570)" (#572) This reverts commit a74d56b8a5d2459760e8a24dce39edf5eabf690c. --- .../FeatureGatedAsyncActionFilter.cs | 56 ++------------ .../FilterCollectionExtensions.cs | 57 +------------- ...rosoft.FeatureManagement.AspNetCore.csproj | 2 +- ...ement.Telemetry.ApplicationInsights.csproj | 2 +- .../FeatureFilters/TimeWindowFilter.cs | 4 +- .../FeatureManagerSnapshot.cs | 4 +- .../Microsoft.FeatureManagement.csproj | 2 +- .../ServiceCollectionExtensions.cs | 3 +- .../FeatureManagementAspNetCore.cs | 41 ---------- .../FeatureManagementTest.cs | 75 ------------------- .../Tests.FeatureManagement/appsettings.json | 12 --- 11 files changed, 19 insertions(+), 239 deletions(-) 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" } }, {