Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,17 @@ When a feature filter is registered to be used for a feature flag, the alias use

This can be overridden through the use of the `FilterAliasAttribute`. A feature filter can be decorated with this attribute to declare the name that should be used in configuration to reference this feature filter within a feature flag.

### Missing Feature Filters

If a feature is configured to be enabled for a specific feature filter and that feature filter hasn't been registered, then an exception will be thrown when the feature is evaluated. The exception can be disabled by using the feature management options.

```
services.Configure<FeatureManagementOptions>(options =>
{
options.IgnoreMissingFeatureFilters = true;
});
```

### Using HttpContext

Feature filters can evaluate whether a feature should be enabled based off the properties of an HTTP Request. This is performed by inspecting the HTTP Context. A feature filter can get a reference to the HTTP Context by obtaining an `IHttpContextAccessor` through dependency injection.
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureManagementError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Microsoft.FeatureManagement
{
/// <summary>
/// An error that can occur during feature management.
/// </summary>
public enum FeatureManagementError
{
/// <summary>
/// A feature filter that was listed for feature evaluation was not found.
/// </summary>
MissingFeatureFilter
}
}
26 changes: 26 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureManagementException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Represents errors that occur during feature management.
/// </summary>
public class FeatureManagementException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="FeatureManagementException"/> class.
/// </summary>
/// <param name="errorType">The feature management error that the exception represents.</param>
/// <param name="message">Error message for the exception.</param>
public FeatureManagementException(FeatureManagementError errorType, string message)
: base(message)
{
Error = errorType;
}

/// <summary>
/// The feature management error that the exception represents.
/// </summary>
public FeatureManagementError Error { get; set; }
}
}
17 changes: 17 additions & 0 deletions src/Microsoft.FeatureManagement/FeatureManagementOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Options that control the behavior of the feature management system.
/// </summary>
public class FeatureManagementOptions
{
/// <summary>
/// Controls the behavior of feature evaluation when dependent feature filters are missing.
/// If missing feature filters are not ignored an exception will be thrown when attempting to evaluate a feature that depends on a missing feature filter.
/// </summary>
public bool IgnoreMissingFeatureFilters { get; set; }
}
}
23 changes: 20 additions & 3 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,22 @@ class FeatureManager : IFeatureManager
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache;
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache;

public FeatureManager(IFeatureSettingsProvider settingsProvider, IEnumerable<IFeatureFilterMetadata> featureFilters, IEnumerable<ISessionManager> sessionManagers, ILoggerFactory loggerFactory)
private readonly FeatureManagementOptions _options;

public FeatureManager(
IFeatureSettingsProvider settingsProvider,
IEnumerable<IFeatureFilterMetadata> featureFilters,
IEnumerable<ISessionManager> sessionManagers,
ILoggerFactory loggerFactory,
IOptions<FeatureManagementOptions> options)
{
_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 = options?.Value ?? throw new ArgumentNullException(nameof(options));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

options?.Value [](start = 23, length = 14)

so we don't expect a default value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value should be returned by Value.

}

public Task<bool> IsEnabledAsync(string feature)
Expand Down Expand Up @@ -78,7 +86,16 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo

if (filter == null)
{
_logger.LogWarning($"Feature filter '{featureFilterSettings.Name}' specified for feature '{feature}' was not found.");
string errorMessage = $"The feature filter '{featureFilterSettings.Name}' specified for feature '{feature}' was not found.";

if (!_options.IgnoreMissingFeatureFilters)
{
throw new FeatureManagementException(FeatureManagementError.MissingFeatureFilter, errorMessage);
}
else
{
_logger.LogWarning(errorMessage);
}

continue;
}
Expand Down
90 changes: 70 additions & 20 deletions tests/Tests.FeatureManagement/FeatureManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class FeatureManagement
private const string OnFeature = "OnTestFeature";
private const string OffFeature = "OffFeature";
private const string ConditionalFeature = "ConditionalFeature";
private const string ContextualFeature = "ContextualFeature";

[Fact]
public async Task ReadsConfiguration()
Expand Down Expand Up @@ -75,18 +76,20 @@ 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<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 +126,14 @@ 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
.AddSingleton(config)
.AddFeatureManagement()
.AddFeatureFilter<TestFilter>();

services.AddMvcCore();
})
services.AddMvcCore();
})
.Configure(app => app.UseMvc()));

IEnumerable<IFeatureFilterMetadata> featureFilters = testServer.Host.Services.GetRequiredService<IEnumerable<IFeatureFilterMetadata>>();
Expand Down Expand Up @@ -269,11 +272,11 @@ public async Task UsesContext()

context.AccountId = "NotEnabledAccount";

Assert.False(await featureManager.IsEnabledAsync(ConditionalFeature, context));
Assert.False(await featureManager.IsEnabledAsync(ContextualFeature, context));

context.AccountId = "abc";

Assert.True(await featureManager.IsEnabledAsync(ConditionalFeature, context));
Assert.True(await featureManager.IsEnabledAsync(ContextualFeature, context));
}

[Fact]
Expand All @@ -297,5 +300,52 @@ public void LimitsFeatureFilterImplementations()
.AddFeatureFilter<InvalidFeatureFilter2>();
});
}

[Fact]
public async Task ThrowsExceptionForMissingFeatureFilter()
{
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>();

FeatureManagementException e = await Assert.ThrowsAsync<FeatureManagementException>(async () => await featureManager.IsEnabledAsync(ConditionalFeature));

Assert.Equal(FeatureManagementError.MissingFeatureFilter, e.Error);
}

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

var services = new ServiceCollection();

services
.Configure<FeatureManagementOptions>(options =>
{
options.IgnoreMissingFeatureFilters = true;
});

services
.AddSingleton(config)
.AddFeatureManagement();

ServiceProvider serviceProvider = services.BuildServiceProvider();

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

var isEnabled = await featureManager.IsEnabledAsync(ConditionalFeature);

Assert.False(isEnabled);
}

}
}
20 changes: 12 additions & 8 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@
"Parameters": {
"P1": "V1"
}
},
{
"Name": "ContextualTest",
"Parameters": {
"AllowedAccounts": [
"abc"
]
}
}
]
},
Expand All @@ -33,6 +25,18 @@
"Name": "Test"
}
]
},
"ContextualFeature": {
"EnabledFor": [
{
"Name": "ContextualTest",
"Parameters": {
"AllowedAccounts": [
"abc"
]
}
}
]
}
}
}