diff --git a/Microsoft.FeatureManagement.sln b/Microsoft.FeatureManagement.sln index bad8dbe5..d4c7ce05 100644 --- a/Microsoft.FeatureManagement.sln +++ b/Microsoft.FeatureManagement.sln @@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TargetingConsoleApp", "exam EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorPages", "examples\RazorPages\RazorPages.csproj", "{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FeatureManagement.AspNetCore", "tests\Tests.FeatureManagement.AspNetCore\Tests.FeatureManagement.AspNetCore.csproj", "{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,10 +55,6 @@ Global {BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Debug|Any CPU.Build.0 = Debug|Any CPU {BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Release|Any CPU.Build.0 = Release|Any CPU - {FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,7 +65,6 @@ Global {E50FB931-7A42-440E-AC47-B8DFE5E15394} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {6558C21E-CF20-4278-AA08-EB9D1DF29D66} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} {BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72} - {FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4} = {8ED6FFEE-4037-49A2-9709-BC519C104A90} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD} diff --git a/build/install-dotnet.ps1 b/build/install-dotnet.ps1 index 11552a78..56196544 100644 --- a/build/install-dotnet.ps1 +++ b/build/install-dotnet.ps1 @@ -1,8 +1,10 @@ -# Installs .NET 6 and .NET 7 for CI/CD environment +# Installs .NET Core 2.1, .NET 5 and .NET 6 for CI/CD environment # see: https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script#examples [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; -&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 6.0 +&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Version 2.1.816 -&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Channel 7.0 \ No newline at end of file +&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) -Version 5.0.408 + +&([scriptblock]::Create((Invoke-WebRequest -UseBasicParsing 'https://dot.net/v1/dotnet-install.ps1'))) diff --git a/examples/RazorPages/RazorPages.csproj b/examples/RazorPages/RazorPages.csproj index e6046fb7..2e72ac40 100644 --- a/examples/RazorPages/RazorPages.csproj +++ b/examples/RazorPages/RazorPages.csproj @@ -1,7 +1,7 @@  - net6.0 + netcoreapp3.1 diff --git a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs index 742b942e..1ffea01a 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs +++ b/src/Microsoft.FeatureManagement.AspNetCore/AspNetCoreFeatureManagementBuilderExtensions.cs @@ -3,6 +3,7 @@ // using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Mvc; using System; using System.Collections.Generic; diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj index 32cf3865..612eecfc 100644 --- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj +++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj @@ -11,7 +11,7 @@ - net6.0;net7.0 + netstandard2.0;netcoreapp3.1;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk @@ -28,9 +28,17 @@ https://aka.ms/AzureAppConfigurationPackageIcon © Microsoft Corporation. All rights reserved. - + + + + + + + + + diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 6d48941e..8e84aecb 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. // using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using System; using System.Collections.Concurrent; @@ -26,15 +25,13 @@ sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider private readonly IConfiguration _configuration; private readonly ConcurrentDictionary _definitions; private IDisposable _changeSubscription; - private readonly ILogger _logger; private int _stale = 0; const string ParseValueErrorString = "Invalid setting '{0}' with value '{1}' for feature '{2}'."; - public ConfigurationFeatureDefinitionProvider(IConfiguration configuration, ILoggerFactory loggerFactory) + public ConfigurationFeatureDefinitionProvider(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); _definitions = new ConcurrentDictionary(); _changeSubscription = ChangeToken.OnChange( @@ -298,18 +295,16 @@ We support private IEnumerable GetFeatureDefinitionSections() { - // - // Look for feature definitions under the "FeatureManagement" section - IConfigurationSection featureManagementConfigurationSection = _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName); - - if (featureManagementConfigurationSection.Exists()) + if (_configuration.GetChildren().Any(s => s.Key.Equals(ConfigurationFields.FeatureManagementSectionName, StringComparison.OrdinalIgnoreCase))) { - return featureManagementConfigurationSection.GetChildren(); + // + // Look for feature definitions under the "FeatureManagement" section + return _configuration.GetSection(ConfigurationFields.FeatureManagementSectionName).GetChildren(); + } + else + { + return _configuration.GetChildren(); } - - _logger.LogDebug($"No configuration section named '{ConfigurationFields.FeatureManagementSectionName}' was found."); - - return Enumerable.Empty(); } private T ParseEnum(string feature, string rawValue, string fieldKeyword) diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index addac954..677c556a 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Microsoft.FeatureManagement.FeatureFilters; using Microsoft.FeatureManagement.Targeting; diff --git a/src/Microsoft.FeatureManagement/IsExternalInit.cs b/src/Microsoft.FeatureManagement/IsExternalInit.cs index 11d19609..279b080b 100644 --- a/src/Microsoft.FeatureManagement/IsExternalInit.cs +++ b/src/Microsoft.FeatureManagement/IsExternalInit.cs @@ -6,7 +6,7 @@ // This class is used to compile .NET frameworks that don't support C# 9.0 or later while still using the init accessor for a property. // The code referenced for this file can be found here: https://github.com/dotnet/roslyn/issues/45510#issuecomment-725091019 -#if NETSTANDARD2_0 || NETSTANDARD2_1 +#if NETSTANDARD2_0 || NETCOREAPP2_1 || NETCOREAPP3_1 using System.ComponentModel; diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj index d33a21a3..383956c9 100644 --- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj +++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj @@ -11,7 +11,7 @@ - netstandard2.0;netstandard2.1 + netstandard2.0;netcoreapp3.1;net5.0;net6.0 true false ..\..\build\Microsoft.FeatureManagement.snk diff --git a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs index 1525c1d8..da5c6d6d 100644 --- a/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs +++ b/src/Microsoft.FeatureManagement/ServiceCollectionExtensions.cs @@ -83,7 +83,7 @@ public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollec throw new ArgumentNullException(nameof(configuration)); } - services.AddSingleton(sp => new ConfigurationFeatureDefinitionProvider(configuration, sp.GetRequiredService())); + services.AddSingleton(new ConfigurationFeatureDefinitionProvider(configuration)); return services.AddFeatureManagement(); } diff --git a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs b/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs deleted file mode 100644 index 7b57bfd7..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/FeatureManagementAspNetCore.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.FeatureManagement; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace Tests.FeatureManagement.AspNetCore -{ - public class FeatureManagementAspNetCore - { - [Fact] - public async Task Integrates() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - - TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => - { - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); - - services.AddMvcCore(o => - { - DisableEndpointRouting(o); - o.Filters.AddForFeature(Enum.GetName(typeof(Features), Features.ConditionalFeature)); - }); - }) - .Configure(app => - { - app.UseForFeature(Enum.GetName(typeof(Features), Features.ConditionalFeature), a => a.Use(async (ctx, next) => - { - ctx.Response.Headers[nameof(RouterMiddleware)] = bool.TrueString; - - await next(); - })); - - app.UseMvc(); - })); - - IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); - - TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); - - testFeatureFilter.Callback = _ => Task.FromResult(true); - - HttpResponseMessage res = await testServer.CreateClient().GetAsync(""); - - Assert.True(res.Headers.Contains(nameof(MvcFilter))); - Assert.True(res.Headers.Contains(nameof(RouterMiddleware))); - - testFeatureFilter.Callback = _ => Task.FromResult(false); - - res = await testServer.CreateClient().GetAsync(""); - - Assert.False(res.Headers.Contains(nameof(MvcFilter))); - Assert.False(res.Headers.Contains(nameof(RouterMiddleware))); - } - - [Fact] - public async Task GatesFeatures() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - - TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => - { - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); - - services.AddMvcCore(o => DisableEndpointRouting(o)); - }) - .Configure(app => app.UseMvc())); - - IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); - - TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); - - // - // Enable all features - testFeatureFilter.Callback = ctx => Task.FromResult(true); - - HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); - HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); - - Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); - - // - // Enable 1/2 features - testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature)); - - gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); - gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); - - Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); - - // - // Enable no - testFeatureFilter.Callback = ctx => Task.FromResult(false); - - gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); - gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); - - Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode); - } - - [Fact] - public async Task GatesRazorPageFeatures() - { - IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - - TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => - { - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); - - services.AddRazorPages(); - - services.AddMvc(o => DisableEndpointRouting(o)); - }) - .Configure(app => - { - app.UseMvc(); - })); - - IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); - - TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); - - // - // Enable all features - testFeatureFilter.Callback = ctx => Task.FromResult(true); - - HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); - HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); - - Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); - - // - // Enable 1/2 features - testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature)); - - gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); - gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); - - Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); - - // - // Enable no - testFeatureFilter.Callback = ctx => Task.FromResult(false); - - gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); - gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); - - Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); - Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode); - } - - private static void DisableEndpointRouting(MvcOptions options) - { - options.EnableEndpointRouting = false; - } - } -} diff --git a/tests/Tests.FeatureManagement.AspNetCore/Features.cs b/tests/Tests.FeatureManagement.AspNetCore/Features.cs deleted file mode 100644 index 5d26be98..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/Features.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -namespace Tests.FeatureManagement.AspNetCore -{ - enum Features - { - ConditionalFeature, - ConditionalFeature2 - } -} diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/_ViewImports.cshtml b/tests/Tests.FeatureManagement.AspNetCore/Pages/_ViewImports.cshtml deleted file mode 100644 index e7acf585..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/Pages/_ViewImports.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@using Tests.FeatureManagement.AspNetCore -@namespace Tests.FeatureManagement.AspNetCore.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/Tests.FeatureManagement.AspNetCore/TestFilter.cs b/tests/Tests.FeatureManagement.AspNetCore/TestFilter.cs deleted file mode 100644 index fbd49122..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/TestFilter.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Microsoft.Extensions.Configuration; -using Microsoft.FeatureManagement; -using System; -using System.Threading.Tasks; - -namespace Tests.FeatureManagement.AspNetCore -{ - class TestFilter : IFeatureFilter, IFilterParametersBinder - { - public Func ParametersBinderCallback { get; set; } - - public Func> Callback { get; set; } - - public object BindParameters(IConfiguration parameters) - { - if (ParametersBinderCallback != null) - { - return ParametersBinderCallback(parameters); - } - - return parameters; - } - - public Task EvaluateAsync(FeatureFilterEvaluationContext context) - { - return Callback?.Invoke(context) ?? Task.FromResult(false); - } - } -} diff --git a/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj b/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj deleted file mode 100644 index c798e1ad..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/Tests.FeatureManagement.AspNetCore.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - net6.0;net7.0 - false - 8.0 - True - ..\..\build\Microsoft.FeatureManagement.snk - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - - diff --git a/tests/Tests.FeatureManagement.AspNetCore/appsettings.json b/tests/Tests.FeatureManagement.AspNetCore/appsettings.json deleted file mode 100644 index ab9e0eb7..00000000 --- a/tests/Tests.FeatureManagement.AspNetCore/appsettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*", - - "FeatureManagement": { - "ConditionalFeature": { - "EnabledFor": [ - { - "Name": "Test", - "Parameters": { - "P1": "V1" - } - } - ] - }, - "ConditionalFeature2": { - "EnabledFor": [ - { - "Name": "Test" - } - ] - } - } -} diff --git a/tests/Tests.FeatureManagement/FeatureManagement.cs b/tests/Tests.FeatureManagement/FeatureManagement.cs index 29f4373e..b690efc9 100644 --- a/tests/Tests.FeatureManagement/FeatureManagement.cs +++ b/tests/Tests.FeatureManagement/FeatureManagement.cs @@ -1,15 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.FeatureFilters; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; +using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -39,9 +44,9 @@ public async Task ReadsConfiguration() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - Assert.True(await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.OnTestFeature))); + Assert.True(await featureManager.IsEnabledAsync(OnFeature)); - Assert.False(await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.OffTestFeature))); + Assert.False(await featureManager.IsEnabledAsync(OffFeature)); IEnumerable featureFilters = serviceProvider.GetRequiredService>(); @@ -57,38 +62,115 @@ public async Task ReadsConfiguration() Assert.Equal("V1", evaluationContext.Parameters["P1"]); - Assert.Equal(Enum.GetName(typeof(Features), Features.ConditionalFeature), evaluationContext.FeatureName); + Assert.Equal(ConditionalFeature, evaluationContext.FeatureName); return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(called); } [Fact] - public async Task ReadsOnlyFeatureManagementSection() + public async Task Integrates() { - MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes("{\"AllowedHosts\": \"*\"}")); - IConfiguration config = new ConfigurationBuilder().AddJsonStream(stream).Build(); + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - var services = new ServiceCollection(); + TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => + { + services + .AddSingleton(config) + .AddFeatureManagement() + .AddFeatureFilter(); - services - .AddSingleton(config) - .AddFeatureManagement() - .AddFeatureFilter(); + services.AddMvcCore(o => + { + DisableEndpointRouting(o); + o.Filters.AddForFeature(ConditionalFeature); + }); + }) + .Configure(app => + { - ServiceProvider serviceProvider = services.BuildServiceProvider(); + app.UseForFeature(ConditionalFeature, a => a.Use(async (ctx, next) => + { + ctx.Response.Headers[nameof(RouterMiddleware)] = bool.TrueString; - IFeatureManager featureManager = serviceProvider.GetRequiredService(); + await next(); + })); - await foreach (string featureName in featureManager.GetFeatureNamesAsync()) - { - // Fail, as no features should be found - Assert.True(false); - } + app.UseMvc(); + })); + + IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); + + TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); + + testFeatureFilter.Callback = _ => Task.FromResult(true); + + HttpResponseMessage res = await testServer.CreateClient().GetAsync(""); + + Assert.True(res.Headers.Contains(nameof(MvcFilter))); + Assert.True(res.Headers.Contains(nameof(RouterMiddleware))); + + testFeatureFilter.Callback = _ => Task.FromResult(false); + + res = await testServer.CreateClient().GetAsync(""); + + Assert.False(res.Headers.Contains(nameof(MvcFilter))); + Assert.False(res.Headers.Contains(nameof(RouterMiddleware))); + } + + [Fact] + public async Task GatesFeatures() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => + { + services + .AddSingleton(config) + .AddFeatureManagement() + .AddFeatureFilter(); + + services.AddMvcCore(o => DisableEndpointRouting(o)); + }) + .Configure(app => app.UseMvc())); + + IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); + + TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); + + // + // Enable all features + testFeatureFilter.Callback = ctx => Task.FromResult(true); + + HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); + HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); + + Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); + + // + // Enable 1/2 features + testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature)); + + gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); + gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); + + Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); + + // + // Enable no + testFeatureFilter.Callback = ctx => Task.FromResult(false); + + gateAllResponse = await testServer.CreateClient().GetAsync("gateAll"); + gateAnyResponse = await testServer.CreateClient().GetAsync("gateAny"); + + Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode); } [Fact] @@ -113,6 +195,60 @@ public async Task CustomFilterContextualTargetingWithNullSetting() Assert.True(await featureManager.IsEnabledAsync("CustomFilterFeature")); } + [Fact] + public async Task GatesRazorPageFeatures() + { + IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services => + { + services + .AddSingleton(config) + .AddFeatureManagement() + .AddFeatureFilter(); + + services.AddMvc(o => DisableEndpointRouting(o)); + }) + .Configure(app => + { + app.UseMvc(); + })); + + IEnumerable featureFilters = testServer.Host.Services.GetRequiredService>(); + + TestFilter testFeatureFilter = (TestFilter)featureFilters.First(f => f is TestFilter); + + // + // Enable all features + testFeatureFilter.Callback = ctx => Task.FromResult(true); + + HttpResponseMessage gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); + HttpResponseMessage gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); + + Assert.Equal(HttpStatusCode.OK, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); + + // + // Enable 1/2 features + testFeatureFilter.Callback = ctx => Task.FromResult(ctx.FeatureName == Enum.GetName(typeof(Features), Features.ConditionalFeature)); + + gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); + gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); + + Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.OK, gateAnyResponse.StatusCode); + + // + // Enable no + testFeatureFilter.Callback = ctx => Task.FromResult(false); + + gateAllResponse = await testServer.CreateClient().GetAsync("RazorTestAll"); + gateAnyResponse = await testServer.CreateClient().GetAsync("RazorTestAny"); + + Assert.Equal(HttpStatusCode.NotFound, gateAllResponse.StatusCode); + Assert.Equal(HttpStatusCode.NotFound, gateAnyResponse.StatusCode); + } + [Fact] public async Task TimeWindow() { @@ -323,11 +459,11 @@ public async Task UsesContext() context.AccountId = "NotEnabledAccount"; - Assert.False(await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ContextualFeature), context)); + Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context)); context.AccountId = "abc"; - Assert.True(await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ContextualFeature), context)); + Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context)); } [Fact] @@ -395,7 +531,7 @@ public async Task ThrowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature))); + FeatureManagementException e = await Assert.ThrowsAsync(async () => await featureManager.IsEnabledAsync(ConditionalFeature)); Assert.Equal(FeatureManagementError.MissingFeatureFilter, e.Error); } @@ -421,7 +557,7 @@ public async Task SwallowsExceptionForMissingFeatureFilter() IFeatureManager featureManager = serviceProvider.GetRequiredService(); - var isEnabled = await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature); Assert.False(isEnabled); } @@ -456,7 +592,7 @@ public async Task CustomFeatureDefinitionProvider() { FeatureDefinition testFeature = new FeatureDefinition { - Name = Enum.GetName(typeof(Features), Features.ConditionalFeature), + Name = ConditionalFeature, EnabledFor = new List() { new FeatureFilterConfiguration @@ -494,12 +630,12 @@ public async Task CustomFeatureDefinitionProvider() Assert.Equal("V1", evaluationContext.Parameters["P1"]); - Assert.Equal(Enum.GetName(typeof(Features), Features.ConditionalFeature), evaluationContext.FeatureName); + Assert.Equal(ConditionalFeature, evaluationContext.FeatureName); return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(called); } @@ -541,7 +677,7 @@ public async Task ThreadsafeSnapshot() for (int i = 0; i < 1000; i++) { - tasks.Add(featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature))); + tasks.Add(featureManager.IsEnabledAsync(ConditionalFeature)); } Assert.True(called); @@ -708,6 +844,8 @@ public async Task RequirementTypeAllExceptions() { IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + string filterOneId = "1"; + var services = new ServiceCollection(); services @@ -751,7 +889,7 @@ public async Task BindsFeatureFlagSettings() { new FeatureDefinition { - Name = Enum.GetName(typeof(Features), Features.ConditionalFeature), + Name = ConditionalFeature, EnabledFor = new List() { testFilterConfiguration @@ -792,7 +930,7 @@ public async Task BindsFeatureFlagSettings() return Task.FromResult(true); }; - await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(binderCalled); @@ -802,7 +940,7 @@ public async Task BindsFeatureFlagSettings() called = false; - await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.False(binderCalled); @@ -816,7 +954,7 @@ public async Task BindsFeatureFlagSettings() called = false; - await featureManager.IsEnabledAsync(Enum.GetName(typeof(Features), Features.ConditionalFeature)); + await featureManager.IsEnabledAsync(ConditionalFeature); Assert.True(binderCalled); @@ -942,5 +1080,14 @@ public async Task VariantsInvalidScenarios() Assert.Equal(FeatureManagementError.InvalidConfigurationSetting, e.Error); Assert.Contains(ConfigurationFields.PercentileAllocationFrom, e.Message); } + + private static void DisableEndpointRouting(MvcOptions options) + { +#if NET6_0 || NET5_0 || NETCOREAPP3_1 + // + // Endpoint routing is disabled by default in .NET Core 2.1 since it didn't exist. + options.EnableEndpointRouting = false; +#endif + } } } diff --git a/tests/Tests.FeatureManagement/Features.cs b/tests/Tests.FeatureManagement/Features.cs index 93847df3..3b2177c1 100644 --- a/tests/Tests.FeatureManagement/Features.cs +++ b/tests/Tests.FeatureManagement/Features.cs @@ -11,7 +11,6 @@ enum Features OffTestFeature, ConditionalFeature, ConditionalFeature2, - ContextualFeature, AnyFilterFeature, AllFilterFeature } diff --git a/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs b/tests/Tests.FeatureManagement/MvcFilter.cs similarity index 91% rename from tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs rename to tests/Tests.FeatureManagement/MvcFilter.cs index 0635a289..40c63131 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/MvcFilter.cs +++ b/tests/Tests.FeatureManagement/MvcFilter.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; -namespace Tests.FeatureManagement.AspNetCore +namespace Tests.FeatureManagement { public class MvcFilter : IAsyncActionFilter { diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAll.cshtml b/tests/Tests.FeatureManagement/Pages/RazorTestAll.cshtml similarity index 100% rename from tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAll.cshtml rename to tests/Tests.FeatureManagement/Pages/RazorTestAll.cshtml diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAll.cshtml.cs b/tests/Tests.FeatureManagement/Pages/RazorTestAll.cshtml.cs similarity index 89% rename from tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAll.cshtml.cs rename to tests/Tests.FeatureManagement/Pages/RazorTestAll.cshtml.cs index 5867aa30..698ee597 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAll.cshtml.cs +++ b/tests/Tests.FeatureManagement/Pages/RazorTestAll.cshtml.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.FeatureManagement.Mvc; -namespace Tests.FeatureManagement.AspNetCore.Pages +namespace Tests.FeatureManagement.Pages { [FeatureGate(Features.ConditionalFeature, Features.ConditionalFeature2)] public class RazorTestAllModel : PageModel diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAny.cshtml b/tests/Tests.FeatureManagement/Pages/RazorTestAny.cshtml similarity index 100% rename from tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAny.cshtml rename to tests/Tests.FeatureManagement/Pages/RazorTestAny.cshtml diff --git a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAny.cshtml.cs b/tests/Tests.FeatureManagement/Pages/RazorTestAny.cshtml.cs similarity index 90% rename from tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAny.cshtml.cs rename to tests/Tests.FeatureManagement/Pages/RazorTestAny.cshtml.cs index 54a96fae..90fa47cf 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/Pages/RazorTestAny.cshtml.cs +++ b/tests/Tests.FeatureManagement/Pages/RazorTestAny.cshtml.cs @@ -6,7 +6,7 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Mvc; -namespace Tests.FeatureManagement.AspNetCore.Pages +namespace Tests.FeatureManagement.Pages { [FeatureGate(RequirementType.Any, Features.ConditionalFeature, Features.ConditionalFeature2)] public class RazorTestAnyModel : PageModel diff --git a/tests/Tests.FeatureManagement/Pages/_ViewImports.cshtml b/tests/Tests.FeatureManagement/Pages/_ViewImports.cshtml new file mode 100644 index 00000000..9999731f --- /dev/null +++ b/tests/Tests.FeatureManagement/Pages/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Tests.FeatureManagement +@namespace Tests.FeatureManagement.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/tests/Tests.FeatureManagement.AspNetCore/TestController.cs b/tests/Tests.FeatureManagement/TestController.cs similarity index 94% rename from tests/Tests.FeatureManagement.AspNetCore/TestController.cs rename to tests/Tests.FeatureManagement/TestController.cs index 2f4c8ce5..5def4cf6 100644 --- a/tests/Tests.FeatureManagement.AspNetCore/TestController.cs +++ b/tests/Tests.FeatureManagement/TestController.cs @@ -5,7 +5,7 @@ using Microsoft.FeatureManagement; using Microsoft.FeatureManagement.Mvc; -namespace Tests.FeatureManagement.AspNetCore +namespace Tests.FeatureManagement { [Route("")] public class TestController : Controller diff --git a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj index 4d8697ad..cbeed357 100644 --- a/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj +++ b/tests/Tests.FeatureManagement/Tests.FeatureManagement.csproj @@ -1,7 +1,7 @@ - + - net48;net6.0;net7.0 + netcoreapp2.1;netcoreapp3.1;net5.0;net6.0 false 9.0 True @@ -9,35 +9,38 @@ - - - - - + + + + + + - - - + + + + - - - + + + + - - + + + + - - Always - +