diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs index 72ed0757..53906497 100644 --- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs +++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Threading; @@ -119,7 +120,7 @@ public Task GetFeatureDefinitionAsync(string featureName) /// An enumerator which provides asynchronous iteration over feature definitions. // // 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 GetAllFeatureDefinitionsAsync() #pragma warning restore CS1998 @@ -300,9 +301,9 @@ private void FindFeatureFlags(IConfiguration configuration, List(configurationSection.Key, rawStatusOverride, MicrosoftFeatureManagementFields.VariantDefinitionStatusOverride); } + var configurationValue = section.GetSection( + MicrosoftFeatureManagementFields.VariantDefinitionConfigurationValue); + var variant = new VariantDefinition() { Name = section[MicrosoftFeatureManagementFields.Name], - ConfigurationValue = section.GetSection(MicrosoftFeatureManagementFields.VariantDefinitionConfigurationValue), + ConfigurationValue = configurationValue, + ConfigurationObject = configurationValue.Exists() + ? CreateConfigurationObject(configurationValue) + : null, StatusOverride = statusOverride }; @@ -602,6 +609,25 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe }; } + private static IReadOnlyDictionary CreateConfigurationObject(IConfigurationSection section) + { + var values = section + .AsEnumerable(makePathsRelative: true) + .Where(x => x.Value != null) + .ToDictionary( + entry => entry.Key, + entry => entry.Value, + StringComparer.OrdinalIgnoreCase); + + // Relative enumeration excludes the section's own value. Preserve it under the empty key. + if (section.Value != null) + { + values.Add(string.Empty, section.Value); + } + + return new ReadOnlyDictionary(values); + } + private static T ParseEnum(string feature, string rawValue, string fieldKeyword) where T : struct, Enum { diff --git a/src/Microsoft.FeatureManagement/FeatureManager.cs b/src/Microsoft.FeatureManagement/FeatureManager.cs index f7bca84a..a6ebed3c 100644 --- a/src/Microsoft.FeatureManagement/FeatureManager.cs +++ b/src/Microsoft.FeatureManagement/FeatureManager.cs @@ -847,10 +847,11 @@ private Variant GetVariantFromVariantDefinition(VariantDefinition variantDefinit variantConfiguration = variantDefinition.ConfigurationValue; } - return new Variant() + return new Variant { Name = variantDefinition.Name, - Configuration = variantConfiguration + Configuration = variantConfiguration, + ConfigurationObject = variantDefinition.ConfigurationObject, }; } } diff --git a/src/Microsoft.FeatureManagement/Variant.cs b/src/Microsoft.FeatureManagement/Variant.cs index f69a47ce..012b5f3b 100644 --- a/src/Microsoft.FeatureManagement/Variant.cs +++ b/src/Microsoft.FeatureManagement/Variant.cs @@ -19,5 +19,11 @@ public class Variant /// The configuration of the variant. /// public IConfigurationSection Configuration { get; set; } + + /// + /// The configuration of the variant. + /// When set, variants should prefer this over . + /// + public object ConfigurationObject { get; set; } } } diff --git a/src/Microsoft.FeatureManagement/VariantDefinition.cs b/src/Microsoft.FeatureManagement/VariantDefinition.cs index 138d2fc5..b26fb13e 100644 --- a/src/Microsoft.FeatureManagement/VariantDefinition.cs +++ b/src/Microsoft.FeatureManagement/VariantDefinition.cs @@ -21,6 +21,14 @@ public class VariantDefinition /// public IConfigurationSection ConfigurationValue { get; set; } + /// + /// A configuration object that can be used as an alternative to . + /// Custom implementations can populate this property directly + /// instead of constructing an instance. + /// When set, variants should prefer this over . + /// + public object ConfigurationObject { get; set; } + /// /// Overrides the state of the feature if this variant has been assigned. /// diff --git a/src/Microsoft.FeatureManagement/VariantExtensions.cs b/src/Microsoft.FeatureManagement/VariantExtensions.cs new file mode 100644 index 00000000..61e2d76b --- /dev/null +++ b/src/Microsoft.FeatureManagement/VariantExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; + +namespace Microsoft.FeatureManagement +{ + /// + /// Extensions for . + /// + public static class VariantExtensions + { + /// + /// Gets the variant configuration as the requested type. + /// + /// The type of the configuration. + /// The variant to read. + /// + /// The supplied configuration object when assignable to ; + /// otherwise, the configuration bound to from . + /// Returns default when the variant or its configuration is absent. + /// + public static T GetConfiguration(this Variant variant) + { + if (variant == null) + { + return default; + } + + if (variant.ConfigurationObject is T typedConfigurationObject) + { + return typedConfigurationObject; + } + + return variant.Configuration != null + ? variant.Configuration.Get() + : default; + } + } +} diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs index a2aaeed1..f6712cd1 100644 --- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs +++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs @@ -2086,6 +2086,15 @@ public async Task UsesVariants() // Test DefaultWhenEnabled and ConfigurationValue with inline IConfigurationSection variant = await featureManager.GetVariantAsync(Features.VariantFeatureDefaultEnabled, cancellationToken); + var configurationObject = (IReadOnlyDictionary)variant.ConfigurationObject; + Assert.True(configurationObject.Keys.ToHashSet().SetEquals(new[] + { + "Size", + "Color", + "Platform:Id", + "Platform:Screens:0", + "Platform:Screens:1" + })); Assert.Equal("Medium", variant.Name); Assert.Equal("450px", variant.Configuration["Size"]); diff --git a/tests/Tests.FeatureManagement/VariantExtensionsTest.cs b/tests/Tests.FeatureManagement/VariantExtensionsTest.cs new file mode 100644 index 00000000..a47b5547 --- /dev/null +++ b/tests/Tests.FeatureManagement/VariantExtensionsTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.FeatureManagement; +using System.Collections.Generic; +using Xunit; + +namespace Tests.FeatureManagement +{ + public class VariantExtensionsTest + { + [Fact] + public void GetConfigurationReturnsNullForNullVariant() + { + Variant variant = null; + + Assert.Null(variant.GetConfiguration()); + } + + [Fact] + public void GetConfigurationPrefersAssignableObject() + { + var supplied = new List { "supplied" }; + + var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource()); + var configurationSection = new ConfigurationSection( + new ConfigurationRoot(new List { provider }), + "Param"); + provider.Set(configurationSection.Key, "42"); + var variant = new Variant + { + ConfigurationObject = supplied, + Configuration = configurationSection + }; + + Assert.Same(supplied, variant.GetConfiguration>()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetConfigurationFallsBackToSection(bool hasIncompatibleObject) + { + var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["Value:AccountId"] = "1", + ["Value:UserId"] = "2", + ["Value:Groups:0"] = "Chrome", + ["Value:Groups:1"] = "Edge", + } + }); + var configurationSection = new ConfigurationSection( + new ConfigurationRoot(new List { provider }), + "Value"); + var variant = new Variant + { + ConfigurationObject = hasIncompatibleObject + ? "some value" + : null, + Configuration = configurationSection + }; + + Assert.Equivalent(new AppContext + { + AccountId = "1", + UserId = "2", + Groups = new List { "Chrome", "Edge" } + }, variant.GetConfiguration()); + } + + [Fact] + public void GetConfigurationBindsScalar() + { + var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource()); + var configurationSection = new ConfigurationSection( + new ConfigurationRoot(new List { provider }), + "Param"); + provider.Set(configurationSection.Key, "42"); + var variant = new Variant + { + Configuration = configurationSection + }; + + Assert.Equal(42, variant.GetConfiguration()); + } + + [Fact] + public void GetConfigurationReturnsNullWithoutConfiguration() + { + Assert.Null(new Variant().GetConfiguration()); + Assert.Null(new Variant().GetConfiguration()); + } + } +} diff --git a/tests/Tests.FeatureManagement/appsettings.json b/tests/Tests.FeatureManagement/appsettings.json index 018ef5c4..9b555e7e 100644 --- a/tests/Tests.FeatureManagement/appsettings.json +++ b/tests/Tests.FeatureManagement/appsettings.json @@ -295,7 +295,11 @@ "name": "Medium", "configuration_value": { "Size": "450px", - "Color": "Purple" + "Color": "Purple", + "Platform": { + "Id": "mobile", + "Screens": ["fullhd", "tablet"] + } } }, { @@ -569,4 +573,4 @@ } ] } -} +}