Skip to content

Commit

Permalink
If a feature is configured to be enabled for a specific feature filte…
Browse files Browse the repository at this point in the history
…r and the feature hasn't been registered, an exception will be thrown when the feature will be evaluated. (microsoft#13)
  • Loading branch information
codapus-co committed Dec 5, 2019
1 parent dd7b9f6 commit 2f2bdb9
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 21 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ Creating a feature filter provides a way to enable features based on criteria th

Feature filters are registered by the `IFeatureManagementBuilder` when `AddFeatureManagement` is called. These feature filters have access to the services that exist within the service collection that was used to add feature flags. Dependency injection can be used to retrieve these services.

If a feature is configured to be enabled for a specific feature filter and the feature hasn't been registered, an exception will be thrown when the feature will be evaluated. The exception could be silently swallowed, using the feature manager's options.

```
services.Configure<FeatureManagerOptions>(options =>
{
options.SwallowExceptionForUnregisteredFilter = true;
});
```

### Parameterized Feature Filters

Some feature filters require parameters to decide whether a feature should be turned on or not. For example a browser feature filter may turn on a feature for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature, while FireFox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `IFeatureFilter.EvaluateAsync`.
Expand Down
8 changes: 6 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license.
//
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
Expand All @@ -21,15 +22,17 @@ class FeatureManager : IFeatureManager
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache;
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache;
private readonly FeatureManagerOptions _options;

public FeatureManager(IFeatureSettingsProvider settingsProvider, IEnumerable<IFeatureFilterMetadata> featureFilters, IEnumerable<ISessionManager> sessionManagers, ILoggerFactory loggerFactory)
public FeatureManager(IFeatureSettingsProvider settingsProvider, IEnumerable<IFeatureFilterMetadata> featureFilters, IEnumerable<ISessionManager> sessionManagers, ILoggerFactory loggerFactory, IOptionsMonitor<FeatureManagerOptions> optionsAccessor)
{
_settingsProvider = settingsProvider;
_featureFilters = featureFilters ?? throw new ArgumentNullException(nameof(featureFilters));
_sessionManagers = sessionManagers ?? throw new ArgumentNullException(nameof(sessionManagers));
_logger = loggerFactory.CreateLogger<FeatureManager>();
_filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>();
_contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>();
_options = optionsAccessor.CurrentValue;
}

public Task<bool> IsEnabledAsync(string feature)
Expand Down Expand Up @@ -78,8 +81,9 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo

if (filter == null)
{
if (!_options.SwallowExceptionForUnregisteredFilter)
throw new Exception($"Feature filter '{featureFilterSettings.Name}' specified for feature '{feature}' was not found.");
_logger.LogWarning($"Feature filter '{featureFilterSettings.Name}' specified for feature '{feature}' was not found.");

continue;
}

Expand Down
16 changes: 16 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureManagerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Options that are being used when a feature is being evaluated by the Feature Manager.
/// </summary>
public class FeatureManagerOptions
{
/// <summary>
/// Is being used to decide if an exception should be thrown or not when a configured feature filter has not been registered.
/// </summary>
public bool SwallowExceptionForUnregisteredFilter { get; set; }
}
}
103 changes: 84 additions & 19 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,26 @@ public async Task Integrates()
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

TestServer testServer = new TestServer(WebHost.CreateDefaultBuilder().ConfigureServices(services =>
{
services
.Configure<FeatureManagerOptions>(options =>
{
options.SwallowExceptionForUnregisteredFilter = true;
});
services
.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();
services.AddMvcCore(o =>
{
o.Filters.AddForFeature<MvcFilter>(ConditionalFeature);
});
})
.Configure(app =>
{
services
.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();
services.AddMvcCore(o => {
o.Filters.AddForFeature<MvcFilter>(ConditionalFeature);
});
})
.Configure(app => {
app.UseForFeature(ConditionalFeature, a => a.Use(async (ctx, next) =>
{
Expand Down Expand Up @@ -123,14 +131,20 @@ 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<TestFilter>();
services.AddMvcCore();
})
{
services
.Configure<FeatureManagerOptions>(options =>
{
options.SwallowExceptionForUnregisteredFilter = true;
});
services
.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();
services.AddMvcCore();
})
.Configure(app => app.UseMvc()));

IEnumerable<IFeatureFilterMetadata> featureFilters = testServer.Host.Services.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>();
Expand Down Expand Up @@ -246,6 +260,12 @@ public async Task UsesContext()

var serviceCollection = new ServiceCollection();

serviceCollection
.Configure<FeatureManagerOptions>(options =>
{
options.SwallowExceptionForUnregisteredFilter = true;
});

serviceCollection.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<ContextualTestFilter>();
Expand Down Expand Up @@ -297,5 +317,50 @@ public void LimitsFeatureFilterImplementations()
.AddFeatureFilter<InvalidFeatureFilter2>();
});
}

[Fact]
public void ThrowExceptionForUnregisteredFilter()
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

var services = new ServiceCollection();

services
.AddSingleton(config)
.AddFeatureManagement();

ServiceProvider serviceProvider = services.BuildServiceProvider();

IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

Assert.ThrowsAsync<Exception>(async () => await featureManager.IsEnabledAsync(ConditionalFeature));
}

[Fact]
public async Task SwallowExceptionForUnregisteredFilter()
{
IConfiguration config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();

var services = new ServiceCollection();

services
.Configure<FeatureManagerOptions>(options =>
{
options.SwallowExceptionForUnregisteredFilter = true;
});

services
.AddSingleton(config)
.AddFeatureManagement();

ServiceProvider serviceProvider = services.BuildServiceProvider();

IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();

var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature);

Assert.False(isEnabled);
}

}
}

0 comments on commit 2f2bdb9

Please sign in to comment.