Skip to content

Commit

Permalink
feat: Adding a Provider implementation on top of the standard dotnet …
Browse files Browse the repository at this point in the history
…FeatureManagement system. (#129)

Signed-off-by: Eric Pattison <eric.pattison@vivint.com>
  • Loading branch information
ericpattison authored Jan 12, 2024
1 parent d0e7ccb commit 69bf2d6
Show file tree
Hide file tree
Showing 9 changed files with 1,217 additions and 0 deletions.
14 changes: 14 additions & 0 deletions DotnetSdkContrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Provide
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagsmith.Test", "test\OpenFeature.Contrib.Providers.Flagsmith.Test\OpenFeature.Contrib.Providers.Flagsmith.Test.csproj", "{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement", "src\OpenFeature.Contrib.Providers.FeatureManagement\OpenFeature.Contrib.Providers.FeatureManagement.csproj", "{2F988A3F-727F-4326-995D-9C123A5E44AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.FeatureManagement.Test", "test\OpenFeature.Contrib.Providers.FeatureManagement.Test\OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj", "{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -61,6 +65,14 @@ Global
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.Build.0 = Release|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F988A3F-727F-4326-995D-9C123A5E44AA}.Release|Any CPU.Build.0 = Release|Any CPU
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -74,5 +86,7 @@ Global
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{47008BEE-7888-4B9B-8884-712A922C3F9B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{2F988A3F-727F-4326-995D-9C123A5E44AA} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{9EBB5E8F-9F05-4DFF-9E99-2BAA5D5325FB} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
EndGlobal
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);
}
}
}
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>
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);
}
}
}
Loading

0 comments on commit 69bf2d6

Please sign in to comment.