-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Adding a Provider implementation on top of the standard dotnet …
…FeatureManagement system. (#129) Signed-off-by: Eric Pattison <[email protected]>
- Loading branch information
1 parent
d0e7ccb
commit 69bf2d6
Showing
9 changed files
with
1,217 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
201 changes: 201 additions & 0 deletions
201
src/OpenFeature.Contrib.Providers.FeatureManagement/FeatureManagementProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.FeatureManagement; | ||
using Microsoft.FeatureManagement.FeatureFilters; | ||
using OpenFeature.Model; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace OpenFeature.Contrib.Providers.FeatureManagement | ||
{ | ||
/// <summary> | ||
/// OpenFeature provider using the Microsoft.FeatureManagement library | ||
/// </summary> | ||
public sealed class FeatureManagementProvider : FeatureProvider | ||
{ | ||
private readonly Metadata metadata = new Metadata("FeatureManagement Provider"); | ||
private readonly IVariantFeatureManager featureManager; | ||
|
||
/// <summary> | ||
/// Create a new instance of the FeatureManagementProvider | ||
/// </summary> | ||
/// <param name="configuration">Provide the Configuration to use as the feature flags.</param> | ||
/// <param name="options">Provide specific FeatureManagementOptions</param> | ||
public FeatureManagementProvider(IConfiguration configuration, FeatureManagementOptions options) | ||
{ | ||
featureManager = new FeatureManager( | ||
new ConfigurationFeatureDefinitionProvider(configuration), | ||
options | ||
); | ||
} | ||
|
||
/// <summary> | ||
/// Create a new instance of the FeatureManagementProvider | ||
/// </summary> | ||
/// <param name="configuration">Provide the Configuration to use as the feature flags.</param> | ||
public FeatureManagementProvider(IConfiguration configuration) : this(configuration, new FeatureManagementOptions()) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Return the Metadata associated with this provider. | ||
/// </summary> | ||
/// <returns>Metadata</returns> | ||
public override Metadata GetMetadata() => metadata; | ||
|
||
/// <inheritdoc /> | ||
public override async Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) | ||
{ | ||
var variant = await Evaluate(flagKey, context, CancellationToken.None); | ||
|
||
if (Boolean.TryParse(variant?.Configuration?.Value, out var value)) | ||
return new ResolutionDetails<bool>(flagKey, value); | ||
return new ResolutionDetails<bool>(flagKey, defaultValue); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override async Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) | ||
{ | ||
var variant = await Evaluate(flagKey, context, CancellationToken.None); | ||
|
||
if (Double.TryParse(variant?.Configuration?.Value, out var value)) | ||
return new ResolutionDetails<double>(flagKey, value); | ||
|
||
return new ResolutionDetails<double>(flagKey, defaultValue); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override async Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) | ||
{ | ||
var variant = await Evaluate(flagKey, context, CancellationToken.None); | ||
|
||
if (int.TryParse(variant?.Configuration?.Value, out var value)) | ||
return new ResolutionDetails<int>(flagKey, value); | ||
|
||
return new ResolutionDetails<int>(flagKey, defaultValue); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override async Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) | ||
{ | ||
var variant = await Evaluate(flagKey, context, CancellationToken.None); | ||
|
||
if (string.IsNullOrEmpty(variant?.Configuration?.Value)) | ||
return new ResolutionDetails<string>(flagKey, defaultValue); | ||
|
||
return new ResolutionDetails<string>(flagKey, variant.Configuration.Value); | ||
} | ||
|
||
/// <inheritdoc /> | ||
public override async Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) | ||
{ | ||
var variant = await Evaluate(flagKey, context, CancellationToken.None); | ||
|
||
if (variant == null) | ||
return new ResolutionDetails<Value>(flagKey, defaultValue); | ||
|
||
Value parsedVariant = ParseVariant(variant); | ||
return new ResolutionDetails<Value>(flagKey, parsedVariant); | ||
} | ||
|
||
/// <inheritdoc /> | ||
private ValueTask<Variant> Evaluate(string flagKey, EvaluationContext evaluationContext, CancellationToken cancellationToken) | ||
{ | ||
TargetingContext targetingContext = ConvertContext(evaluationContext); | ||
if (targetingContext != null) | ||
return featureManager.GetVariantAsync(flagKey, targetingContext, cancellationToken); | ||
return featureManager.GetVariantAsync(flagKey, CancellationToken.None); | ||
} | ||
|
||
/// <summary> | ||
/// Converts the OpenFeature EvaluationContext to the Microsoft.FeatureManagement TargetingContext | ||
/// </summary> | ||
/// <param name="evaluationContext"></param> | ||
/// <returns></returns> | ||
private TargetingContext ConvertContext(EvaluationContext evaluationContext) | ||
{ | ||
if (evaluationContext == null) | ||
return null; | ||
|
||
TargetingContext targetingContext = new TargetingContext(); | ||
|
||
if (evaluationContext.ContainsKey(nameof(targetingContext.UserId))) | ||
{ | ||
Value userId = evaluationContext.GetValue(nameof(targetingContext.UserId)); | ||
if (userId.IsString) targetingContext.UserId = userId.AsString; | ||
} | ||
|
||
if (evaluationContext.ContainsKey(nameof(targetingContext.Groups))) | ||
{ | ||
Value groups = evaluationContext.GetValue(nameof(targetingContext.Groups)); | ||
if (groups.IsList) | ||
{ | ||
List<string> groupList = new List<string>(); | ||
foreach (var group in groups.AsList) | ||
{ | ||
if (group.IsString) groupList.Add(group.AsString); | ||
} | ||
targetingContext.Groups = groupList; | ||
} | ||
} | ||
|
||
return targetingContext; | ||
} | ||
|
||
/// <summary> | ||
/// Parses an Microsoft.FeatureManagement Variant into an OpenFeature Value | ||
/// </summary> | ||
/// <param name="variant"></param> | ||
/// <returns></returns> | ||
private Value ParseVariant(Variant variant) | ||
{ | ||
if (variant == null || variant.Configuration == null) | ||
return null; | ||
|
||
if (variant.Configuration.Value == null) | ||
return ParseChildren(variant.Configuration.GetChildren()); | ||
|
||
return ParseUnknownType(variant.Configuration.Value); | ||
} | ||
|
||
/// <summary> | ||
/// Iterataes over a Variants configuration to parse it into an OpenFeature Value | ||
/// </summary> | ||
/// <param name="children"></param> | ||
/// <returns></returns> | ||
private Value ParseChildren(IEnumerable<IConfigurationSection> children) | ||
{ | ||
IDictionary<string, Value> keyValuePairs = new Dictionary<string, Value>(); | ||
if (children == null) return null; | ||
foreach (var child in children) | ||
{ | ||
if (child.Value != null) | ||
keyValuePairs.Add(child.Key, ParseUnknownType(child.Value)); | ||
if (child.GetChildren().Any()) | ||
keyValuePairs.Add(child.Key, ParseChildren(child.GetChildren())); | ||
} | ||
return new Value(new Structure(keyValuePairs)); | ||
} | ||
|
||
/// <summary> | ||
/// Attempts to parse an arbitrary string value through a set of parsable types | ||
/// </summary> | ||
/// <param name="value"></param> | ||
/// <returns></returns> | ||
private Value ParseUnknownType(string value) | ||
{ | ||
if (bool.TryParse(value, out bool boolResult)) | ||
return new Value(boolResult); | ||
if (double.TryParse(value, out double doubleResult)) | ||
return new Value(doubleResult); | ||
if (int.TryParse(value, out int intResult)) | ||
return new Value(intResult); | ||
if (DateTime.TryParse(value, out DateTime dateTimeResult)) | ||
return new Value(dateTimeResult); | ||
|
||
return new Value(value); | ||
} | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...ontrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<PackageId>OpenFeature.Contrib.Provider.FeatureManagement</PackageId> | ||
<VersionNumber>0.0.1</VersionNumber> | ||
<Version>$(VersionNumber)-preview</Version> | ||
<AssemblyVersion>$(VersionNumber)</AssemblyVersion> | ||
<FileVersion>$(VersionNumber)</FileVersion> | ||
<Description>An OpenFeature Provider built on top of the standard Microsoft FeatureManagement Library</Description> | ||
<PackageProjectUrl>https://openfeature.dev</PackageProjectUrl> | ||
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk-contrib</RepositoryUrl> | ||
<Authors>Eric Pattison</Authors> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.FeatureManagement" Version="4.0.0-preview" /> | ||
</ItemGroup> | ||
|
||
</Project> |
103 changes: 103 additions & 0 deletions
103
...eature.Contrib.Providers.FeatureManagement.Test/FeatureManagementProviderTestNoContext.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
using Microsoft.Extensions.Configuration; | ||
using System.Threading.Tasks; | ||
using Xunit; | ||
|
||
namespace OpenFeature.Contrib.Providers.FeatureManagement.Test | ||
{ | ||
public class FeatureManagementProviderTestNoContext | ||
{ | ||
[Theory] | ||
[MemberData(nameof(TestData.BooleanNoContext), MemberType = typeof(TestData))] | ||
public async Task BooleanValue_ShouldReturnExpected(string key, bool defaultValue, bool expectedValue) | ||
{ | ||
// Arrange | ||
var configuration = new ConfigurationBuilder() | ||
.AddJsonFile("appsettings.enabled.json") | ||
.Build(); | ||
|
||
var provider = new FeatureManagementProvider(configuration); | ||
|
||
// Act | ||
// Invert the expected value to ensure that the value is being read from the configuration | ||
var result = await provider.ResolveBooleanValue(key, defaultValue); | ||
|
||
// Assert | ||
Assert.Equal(expectedValue, result.Value); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TestData.DoubleNoContext), MemberType = typeof(TestData))] | ||
public async Task DoubleValue_ShouldReturnExpected(string key, double defaultValue, double expectedValue) | ||
{ | ||
// Arrange | ||
var configuration = new ConfigurationBuilder() | ||
.AddJsonFile("appsettings.enabled.json") | ||
.Build(); | ||
var provider = new FeatureManagementProvider(configuration); | ||
|
||
// Act | ||
// Using 0.0 for the default to verify the value is being read from the configuration | ||
var result = await provider.ResolveDoubleValue(key, defaultValue); | ||
|
||
// Assert | ||
Assert.Equal(expectedValue, result.Value); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TestData.IntegerNoContext), MemberType = typeof(TestData))] | ||
public async Task IntegerValue_ShouldReturnExpected(string key, int defaultValue, int expectedValue) | ||
{ | ||
// Arrange | ||
var configuration = new ConfigurationBuilder() | ||
.AddJsonFile("appsettings.enabled.json") | ||
.Build(); | ||
var provider = new FeatureManagementProvider(configuration); | ||
|
||
// Act | ||
// Using 0 for the default to verify the value is being read from the configuration | ||
var result = await provider.ResolveIntegerValue(key, defaultValue); | ||
|
||
// Assert | ||
Assert.Equal(expectedValue, result.Value); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TestData.StringNoContext), MemberType = typeof(TestData))] | ||
public async Task StringValue_ShouldReturnExpected(string key, string defaultValue, string expectedValue) | ||
{ | ||
// Arrange | ||
var configuration = new ConfigurationBuilder() | ||
.AddJsonFile("appsettings.enabled.json") | ||
.Build(); | ||
var provider = new FeatureManagementProvider(configuration); | ||
|
||
// Act | ||
// Using 0 for the default to verify the value is being read from the configuration | ||
var result = await provider.ResolveStringValue(key, defaultValue); | ||
|
||
// Assert | ||
Assert.Equal(expectedValue, result.Value); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(TestData.StructureNoContext), MemberType = typeof(TestData))] | ||
public async Task StructureValue_ShouldReturnExpected(string key) | ||
{ | ||
// Arrange | ||
var configuration = new ConfigurationBuilder() | ||
.AddJsonFile("appsettings.enabled.json") | ||
.Build(); | ||
var provider = new FeatureManagementProvider(configuration); | ||
|
||
// Act | ||
// Using 0 for the default to verify the value is being read from the configuration | ||
var result = await provider.ResolveStructureValue(key, null); | ||
|
||
// Assert | ||
Assert.NotNull(result); | ||
Assert.NotNull(result.Value); | ||
Assert.True(result.Value.IsStructure); | ||
Assert.Equal(2, result.Value.AsStructure.Count); | ||
} | ||
} | ||
} |
Oops, something went wrong.