Skip to content

Commit

Permalink
Adds 'Settings' to Context and uses it as a cache of parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
rossgrambo committed Apr 21, 2023
1 parent e79252b commit 48d58a2
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ We support
enabledFor.Add(new FeatureFilterConfiguration()
{
Name = section[nameof(FeatureFilterConfiguration.Name)],
Parameters = section.GetSection(nameof(FeatureFilterConfiguration.Parameters))
Parameters = new ConfigurationWrapper(section.GetSection(nameof(FeatureFilterConfiguration.Parameters)))
});
}
}
Expand Down
38 changes: 38 additions & 0 deletions src/Microsoft.FeatureManagement/ConfigurationWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Wraps an instance of IConfiguration.
/// </summary>
class ConfigurationWrapper : IConfiguration
{
private readonly IConfiguration _configuration;

public ConfigurationWrapper(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public string this[string key]
{
get => _configuration[key];
set => _configuration[key] = value;
}

public IEnumerable<IConfigurationSection> GetChildren() =>
_configuration.GetChildren();

public IChangeToken GetReloadToken() =>
_configuration.GetReloadToken();

public IConfigurationSection GetSection(string key) =>
_configuration.GetSection(key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ public class FeatureFilterEvaluationContext
/// The settings provided for the feature filter to use when evaluating whether the feature should be enabled.
/// </summary>
public IConfiguration Parameters { get; set; }

/// <summary>
/// A settings object, if any, that has been pre-bound from <see cref="Parameters"/>.
/// The settings are made available for <see cref="IFeatureFilter"/>'s that implement <see cref="IFilterParametersBinder"/>.
/// </summary>
public object Settings { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Configuration;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// An interface used by the feature management system to pre-bind feature filter parameters to a settings type.
/// <see cref="IFeatureFilter"/>s can implement this interface to take advantage of caching of settings by the feature management system.
/// </summary>
public interface IFilterParametersBinder
{
/// <summary>
/// Binds a set of feature filter parameters to a settings object.
/// </summary>
/// <param name="parameters">The configuration representing filter parameters to bind to a settings object</param>
/// <returns>A settings object that is understood by the implementer of <see cref="IFilterParametersBinder"/>.</returns>
object BindParameters(IConfiguration parameters);
}
}
14 changes: 12 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureFilters/PercentageFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate a feature based on a random percentage.
/// </summary>
[FilterAlias(Alias)]
public class PercentageFilter : IFeatureFilter
public class PercentageFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.Percentage";
private readonly ILogger _logger;
Expand All @@ -26,14 +26,24 @@ public PercentageFilter(ILoggerFactory loggerFactory)
_logger = loggerFactory.CreateLogger<PercentageFilter>();
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="PercentageFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="PercentageFilterSettings"/>.</param>
/// <returns><see cref="PercentageFilterSettings"/> that can later be used in feature evaluation.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<PercentageFilterSettings>() ?? new PercentageFilterSettings();
}

/// <summary>
/// Performs a percentage based evaluation to determine whether a feature is enabled.
/// </summary>
/// <param name="context">The feature evaluation context.</param>
/// <returns>True if the feature is enabled, false otherwise.</returns>
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
PercentageFilterSettings settings = context.Parameters.Get<PercentageFilterSettings>() ?? new PercentageFilterSettings();
PercentageFilterSettings settings = (PercentageFilterSettings)context.Settings;

bool result = true;

Expand Down
14 changes: 12 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureFilters/TimeWindowFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate a feature based on a time window.
/// </summary>
[FilterAlias(Alias)]
public class TimeWindowFilter : IFeatureFilter
public class TimeWindowFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.TimeWindow";
private readonly ILogger _logger;
Expand All @@ -26,14 +26,24 @@ public TimeWindowFilter(ILoggerFactory loggerFactory)
_logger = loggerFactory.CreateLogger<TimeWindowFilter>();
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TimeWindowFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TimeWindowFilterSettings"/>.</param>
/// <returns><see cref="TimeWindowFilterSettings"/> that can later be used in feature evaluation.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TimeWindowFilterSettings>() ?? new TimeWindowFilterSettings();
}

/// <summary>
/// Evaluates whether a feature is enabled based on a configurable time window.
/// </summary>
/// <param name="context">The feature evaluation context.</param>
/// <returns>True if the feature is enabled, false otherwise.</returns>
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
TimeWindowFilterSettings settings = context.Parameters.Get<TimeWindowFilterSettings>() ?? new TimeWindowFilterSettings();
TimeWindowFilterSettings settings = (TimeWindowFilterSettings)context.Settings;

DateTimeOffset now = DateTimeOffset.UtcNow;

Expand Down
80 changes: 73 additions & 7 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
Expand All @@ -14,15 +16,24 @@ namespace Microsoft.FeatureManagement
/// <summary>
/// Used to evaluate whether a feature is enabled or disabled.
/// </summary>
class FeatureManager : IFeatureManager
class FeatureManager : IFeatureManager, IDisposable
{
private readonly TimeSpan SettingsCachePeriod = TimeSpan.FromSeconds(5);
private readonly IFeatureDefinitionProvider _featureDefinitionProvider;
private readonly IEnumerable<IFeatureFilterMetadata> _featureFilters;
private readonly IEnumerable<ISessionManager> _sessionManagers;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache;
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache;
private readonly FeatureManagementOptions _options;
private readonly IMemoryCache _cache;

private class ConfigurationCacheItem
{
public IConfiguration Parameters { get; set; }

public object Settings { get; set; }
}

public FeatureManager(
IFeatureDefinitionProvider featureDefinitionProvider,
Expand All @@ -38,6 +49,12 @@ public FeatureManager(
_filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>();
_contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>();
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_cache = new MemoryCache(
Options.Create(
new MemoryCacheOptions
{
ExpirationScanFrequency = SettingsCachePeriod
}));
}

public Task<bool> IsEnabledAsync(string feature)
Expand All @@ -57,6 +74,10 @@ public async IAsyncEnumerable<string> GetFeatureNamesAsync()
yield return featureDefintion.Name;
}
}
public void Dispose()
{
_cache.Dispose();
}

private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appContext, bool useAppContext)
{
Expand Down Expand Up @@ -112,7 +133,7 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
enabled = true;
break;
}

continue;
}

Expand Down Expand Up @@ -144,7 +165,9 @@ private async Task<bool> IsEnabledAsync<TContext>(string feature, TContext appCo
{
ContextualFeatureFilterEvaluator contextualFilter = GetContextualFeatureFilter(featureFilterConfiguration.Name, typeof(TContext));

if (contextualFilter != null &&
BindSettings(filter, context);

if (contextualFilter != null &&
await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false) == targetEvaluation)
{
enabled = targetEvaluation;
Expand All @@ -155,12 +178,15 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false)

//
// IFeatureFilter
if (filter is IFeatureFilter featureFilter &&
await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation)
if (filter is IFeatureFilter featureFilter)
{
enabled = targetEvaluation;
BindSettings(filter, context);

break;
if (await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvaluation) {
enabled = targetEvaluation;

break;
}
}
}
}
Expand All @@ -187,6 +213,46 @@ await featureFilter.EvaluateAsync(context).ConfigureAwait(false) == targetEvalua
return enabled;
}

private void BindSettings(IFeatureFilterMetadata filter, FeatureFilterEvaluationContext context)
{
IFilterParametersBinder binder = filter as IFilterParametersBinder;

if (binder == null)
{
return;
}

object settings;

//
// Check if settings already bound from configuration
ConfigurationCacheItem cacheItem = (ConfigurationCacheItem)_cache.Get(context.FeatureName);

if (cacheItem == null ||
cacheItem.Parameters != context.Parameters)
{
settings = binder.BindParameters(context.Parameters);

_cache.Set(
context.FeatureName,
new ConfigurationCacheItem
{
Settings = settings,
Parameters = context.Parameters
},
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = SettingsCachePeriod
});
}
else
{
settings = cacheItem.Settings;
}

context.Settings = settings;
}

private IFeatureFilterMetadata GetFeatureFilterMetadata(string filterName)
{
const string filterSuffix = "filter";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.1.23" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate features for targeted audiences.
/// </summary>
[FilterAlias(Alias)]
public class ContextualTargetingFilter : IContextualFeatureFilter<ITargetingContext>
public class ContextualTargetingFilter : IContextualFeatureFilter<ITargetingContext>, IFilterParametersBinder
{
private const string Alias = "Microsoft.Targeting";
private readonly TargetingEvaluationOptions _options;
Expand All @@ -37,6 +37,16 @@ public ContextualTargetingFilter(IOptions<TargetingEvaluationOptions> options, I
private StringComparison ComparisonType => _options.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
private StringComparer ComparerType => _options.IgnoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TargetingFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TargetingFilterSettings"/>.</param>
/// <returns><see cref="TargetingFilterSettings"/> that can later be used in targeting.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
}

/// <summary>
/// Performs a targeting evaluation using the provided <see cref="TargetingContext"/> to determine if a feature should be enabled.
/// </summary>
Expand All @@ -56,7 +66,7 @@ public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context, ITargeti
throw new ArgumentNullException(nameof(targetingContext));
}

TargetingFilterSettings settings = context.Parameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
TargetingFilterSettings settings = (TargetingFilterSettings)context.Settings;

if (!TryValidateSettings(settings, out string paramName, out string message))
{
Expand Down
13 changes: 12 additions & 1 deletion src/Microsoft.FeatureManagement/Targeting/TargetingFilter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
Expand All @@ -12,7 +13,7 @@ namespace Microsoft.FeatureManagement.FeatureFilters
/// A feature filter that can be used to activate features for targeted audiences.
/// </summary>
[FilterAlias(Alias)]
public class TargetingFilter : IFeatureFilter
public class TargetingFilter : IFeatureFilter, IFilterParametersBinder
{
private const string Alias = "Microsoft.Targeting";
private readonly ITargetingContextAccessor _contextAccessor;
Expand All @@ -32,6 +33,16 @@ public TargetingFilter(IOptions<TargetingEvaluationOptions> options, ITargetingC
_logger = loggerFactory?.CreateLogger<TargetingFilter>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}

/// <summary>
/// Binds configuration representing filter parameters to <see cref="TargetingFilterSettings"/>.
/// </summary>
/// <param name="filterParameters">The configuration representing filter parameters that should be bound to <see cref="TargetingFilterSettings"/>.</param>
/// <returns><see cref="TargetingFilterSettings"/> that can later be used in targeting.</returns>
public object BindParameters(IConfiguration filterParameters)
{
return filterParameters.Get<TargetingFilterSettings>() ?? new TargetingFilterSettings();
}

/// <summary>
/// Performs a targeting evaluation using the current <see cref="TargetingContext"/> to determine if a feature should be enabled.
/// </summary>
Expand Down
Loading

0 comments on commit 48d58a2

Please sign in to comment.