Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName)
/// <returns>An enumerator which provides asynchronous iteration over feature definitions.</returns>
//
// The async key word is necessary for creating IAsyncEnumerable.
// The need to disable this warning occurs when implementing async stream synchronously.
// The need to disable this warning occurs when implementing async stream synchronously.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async IAsyncEnumerable<FeatureDefinition> GetAllFeatureDefinitionsAsync()
#pragma warning restore CS1998
Expand Down Expand Up @@ -297,9 +297,9 @@ private void FindFeatureDefinitionSources(
private FeatureDefinition ParseDotnetSchemaFeatureDefinition(IConfigurationSection configurationSection)
{
/*

We support

myFeature: {
enabledFor: [{name: "myFeatureFilter1"}, {name: "myFeatureFilter2"}]
},
Expand Down Expand Up @@ -388,7 +388,7 @@ We support
private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSection configurationSection)
{
/*

If Microsoft feature flag schema is enabled, we support

FeatureFlags: [
Expand Down Expand Up @@ -544,6 +544,8 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe
{
StatusOverride statusOverride = StatusOverride.None;

IConfigurationSection variantConfiguration = section.GetSection(MicrosoftFeatureManagementFields.VariantDefinitionConfigurationValue);

string rawStatusOverride = section[MicrosoftFeatureManagementFields.VariantDefinitionStatusOverride];

if (!string.IsNullOrEmpty(rawStatusOverride))
Expand All @@ -554,7 +556,10 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe
var variant = new VariantDefinition()
{
Name = section[MicrosoftFeatureManagementFields.Name],
ConfigurationValue = section.GetSection(MicrosoftFeatureManagementFields.VariantDefinitionConfigurationValue),
ConfigurationValue = variantConfiguration,
ConfigurationCache = variantConfiguration.Exists()
? new VariantConfigurationCache(variantConfiguration)
: null,
StatusOverride = statusOverride
};

Expand Down
19 changes: 15 additions & 4 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -842,15 +842,26 @@ private Variant GetVariantFromVariantDefinition(VariantDefinition variantDefinit
{
IConfigurationSection variantConfiguration = null;

if (variantDefinition.ConfigurationValue.Exists())
IConfigurationSection definitionConfiguration = variantDefinition.ConfigurationValue;

if (definitionConfiguration?.Exists() == true)
{
variantConfiguration = definitionConfiguration;
}

VariantConfigurationCache configurationCache = variantDefinition.ConfigurationCache;

if (!ReferenceEquals(configurationCache?.Configuration, variantConfiguration))
{
variantConfiguration = variantDefinition.ConfigurationValue;
configurationCache = null;
}

return new Variant()
return new Variant
{
Name = variantDefinition.Name,
Configuration = variantConfiguration
Configuration = variantConfiguration,
ConfigurationObject = variantDefinition.ConfigurationObject,
ConfigurationCache = configurationCache,
};
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/Microsoft.FeatureManagement/Variant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,13 @@ public class Variant
/// The configuration of the variant.
/// </summary>
public IConfigurationSection Configuration { get; set; }

/// <summary>
/// The configuration of the variant.
/// When set, variants should prefer this over <see cref="Configuration"/>.
/// </summary>
public object ConfigurationObject { get; set; }

internal VariantConfigurationCache ConfigurationCache { get; set; }
}
}
74 changes: 74 additions & 0 deletions src/Microsoft.FeatureManagement/VariantConfigurationCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace Microsoft.FeatureManagement
{
internal sealed class VariantConfigurationCache
{
private readonly ConcurrentDictionary<Type, Lazy<object>> _configurations =
new ConcurrentDictionary<Type, Lazy<object>>();

public VariantConfigurationCache(IConfigurationSection configuration)
{
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public IConfigurationSection Configuration { get; }

public T GetConfiguration<T>()
{
Type configurationType = typeof(T);

if (_configurations.TryGetValue(configurationType, out Lazy<object> configuration))
{
return GetValue<T>(configurationType, configuration);
}

return GetOrAdd(() => Configuration.Get<T>());
}

internal T GetOrAdd<T>(Func<T> valueFactory)
{
if (valueFactory == null)
{
throw new ArgumentNullException(nameof(valueFactory));
}

Type configurationType = typeof(T);

if (!_configurations.TryGetValue(configurationType, out Lazy<object> configuration))
{
var newConfiguration = new Lazy<object>(
() => valueFactory(),
LazyThreadSafetyMode.ExecutionAndPublication);

configuration = _configurations.GetOrAdd(configurationType, newConfiguration);
}

return GetValue<T>(configurationType, configuration);
}

private T GetValue<T>(Type configurationType, Lazy<object> configuration)
{
try
{
return (T)configuration.Value;
}
catch
{
// A failed binding should not permanently poison this configuration type.
((ICollection<KeyValuePair<Type, Lazy<object>>>)_configurations).Remove(
new KeyValuePair<Type, Lazy<object>>(configurationType, configuration));

throw;
}
}
}
}
10 changes: 10 additions & 0 deletions src/Microsoft.FeatureManagement/VariantDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ public class VariantDefinition
/// </summary>
public IConfigurationSection ConfigurationValue { get; set; }

/// <summary>
/// A configuration object that can be used as an alternative to <see cref="ConfigurationValue"/>.
/// Custom <see cref="IFeatureDefinitionProvider"/> implementations can populate this property directly
/// instead of constructing an <see cref="IConfiguration"/> instance.
/// When set, variants should prefer this over <see cref="ConfigurationValue"/>.
/// </summary>
public object ConfigurationObject { get; set; }

internal VariantConfigurationCache ConfigurationCache { get; set; }

/// <summary>
/// Overrides the state of the feature if this variant has been assigned.
/// </summary>
Expand Down
52 changes: 52 additions & 0 deletions src/Microsoft.FeatureManagement/VariantExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//

using Microsoft.Extensions.Configuration;

namespace Microsoft.FeatureManagement
{
/// <summary>
/// Extensions for <see cref="Variant"/>.
/// </summary>
public static class VariantExtensions
{
/// <summary>
/// Gets the variant configuration as the requested type.
/// </summary>
/// <typeparam name="T">The type of the configuration.</typeparam>
/// <param name="variant">The variant to read.</param>
/// <returns>
/// The supplied configuration object when <see cref="Variant.ConfigurationObject"/> assignable to <typeparamref name="T"/>;
/// otherwise, the configuration bound to <typeparamref name="T"/> from <see cref="Variant.Configuration"/>.
/// Returns <c>default</c> when the variant or its configuration is absent.
/// </returns>
/// <remarks>
/// Provider-backed variants created from the same feature definition may share cached configuration instances.
/// Reloaded feature definitions use a new cache.
/// Callers should treat the returned configuration as read-only.
/// </remarks>
public static T GetConfiguration<T>(this Variant variant)
{
if (variant == null)
{
return default;
}

if (variant.ConfigurationObject is T typedConfigurationObject)
{
return typedConfigurationObject;
}

if (variant.ConfigurationCache != null &&
ReferenceEquals(variant.ConfigurationCache.Configuration, variant.Configuration))
{
return variant.ConfigurationCache.GetConfiguration<T>();
}

return variant.Configuration != null
? variant.Configuration.Get<T>()
: default;
}
}
}
1 change: 1 addition & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,7 @@ public async Task UsesVariants()
variant = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken);

Assert.Equal("Medium", variant.Name);
Assert.Null(variant.ConfigurationObject);
Assert.Equal("450px", variant.Configuration["Size"]);
Assert.True(await featureManager.IsEnabledAsync(Features.VariantFeatureDefaultEnabled, cancellationToken));

Expand Down
Loading