Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,7 +120,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 @@ -300,9 +301,9 @@ private void FindFeatureFlags(IConfiguration configuration, List<IConfigurationS
private FeatureDefinition ParseDotnetSchemaFeatureDefinition(IConfigurationSection configurationSection)
{
/*

We support

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

If Microsoft feature flag schema is enabled, we support

FeatureFlags: [
Expand Down Expand Up @@ -554,10 +555,16 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe
statusOverride = ParseEnum<StatusOverride>(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
};

Expand Down Expand Up @@ -602,6 +609,25 @@ private FeatureDefinition ParseMicrosoftSchemaFeatureDefinition(IConfigurationSe
};
}

private static IReadOnlyDictionary<string, string> 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<string, string>(values);
}

private static T ParseEnum<T>(string feature, string rawValue, string fieldKeyword)
where T : struct, Enum
{
Expand Down
5 changes: 3 additions & 2 deletions src/Microsoft.FeatureManagement/FeatureManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Microsoft.FeatureManagement/Variant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ 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; }
}
}
8 changes: 8 additions & 0 deletions src/Microsoft.FeatureManagement/VariantDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ 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; }

/// <summary>
/// Overrides the state of the feature if this variant has been assigned.
/// </summary>
Expand Down
40 changes: 40 additions & 0 deletions src/Microsoft.FeatureManagement/VariantExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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>
public static T GetConfiguration<T>(this Variant variant)
{
if (variant == null)
{
return default;
}

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

return variant.Configuration != null
? variant.Configuration.Get<T>()
: default;
}
}
}
9 changes: 9 additions & 0 deletions tests/Tests.FeatureManagement/FeatureManagementTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>)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"]);
Expand Down
98 changes: 98 additions & 0 deletions tests/Tests.FeatureManagement/VariantExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -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<string>());
}

[Fact]
public void GetConfigurationPrefersAssignableObject()
{
var supplied = new List<string> { "supplied" };

var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource());
var configurationSection = new ConfigurationSection(
new ConfigurationRoot(new List<IConfigurationProvider> { provider }),
"Param");
provider.Set(configurationSection.Key, "42");
var variant = new Variant
{
ConfigurationObject = supplied,
Configuration = configurationSection
};

Assert.Same(supplied, variant.GetConfiguration<IReadOnlyList<string>>());
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void GetConfigurationFallsBackToSection(bool hasIncompatibleObject)
{
var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource
{
InitialData = new Dictionary<string, string>
{
["Value:AccountId"] = "1",
["Value:UserId"] = "2",
["Value:Groups:0"] = "Chrome",
["Value:Groups:1"] = "Edge",
}
});
var configurationSection = new ConfigurationSection(
new ConfigurationRoot(new List<IConfigurationProvider> { provider }),
"Value");
var variant = new Variant
{
ConfigurationObject = hasIncompatibleObject
? "some value"
: null,
Configuration = configurationSection
};

Assert.Equivalent(new AppContext
{
AccountId = "1",
UserId = "2",
Groups = new List<string> { "Chrome", "Edge" }
}, variant.GetConfiguration<AppContext>());
}

[Fact]
public void GetConfigurationBindsScalar()
{
var provider = new MemoryConfigurationProvider(new MemoryConfigurationSource());
var configurationSection = new ConfigurationSection(
new ConfigurationRoot(new List<IConfigurationProvider> { provider }),
"Param");
provider.Set(configurationSection.Key, "42");
var variant = new Variant
{
Configuration = configurationSection
};

Assert.Equal(42, variant.GetConfiguration<int>());
}

[Fact]
public void GetConfigurationReturnsNullWithoutConfiguration()
{
Assert.Null(new Variant().GetConfiguration<string>());
Assert.Null(new Variant().GetConfiguration<int?>());
}
}
}
8 changes: 6 additions & 2 deletions tests/Tests.FeatureManagement/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,11 @@
"name": "Medium",
"configuration_value": {
"Size": "450px",
"Color": "Purple"
"Color": "Purple",
"Platform": {
"Id": "mobile",
"Screens": ["fullhd", "tablet"]
}
}
},
{
Expand Down Expand Up @@ -569,4 +573,4 @@
}
]
}
}
}