Skip to content

Commit

Permalink
Finished initial implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Eric Pattison <[email protected]>
  • Loading branch information
ericpattison committed Jan 9, 2024
1 parent dd400df commit 729632c
Show file tree
Hide file tree
Showing 10 changed files with 1,078 additions and 37 deletions.
9 changes: 0 additions & 9 deletions src/OpenFeature.Contrib.Providers.FeatureManagement/Class1.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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;

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);
}

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);
}

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);
}

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);
}

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);
}

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);
}

private TargetingContext ConvertContext(EvaluationContext evaluationContext)
{
if (evaluationContext == null)
return null;

TargetingContext targetingContext = new TargetingContext();
if(evaluationContext.ContainsKey("UserId"))
{
Value userId = evaluationContext.GetValue("UserId");
if (userId.IsString) targetingContext.UserId = userId.AsString;
}

if(evaluationContext.ContainsKey("Groups"))
{
Value groups = evaluationContext.GetValue("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;
}

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);
}

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));
}

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
@@ -1,8 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>7.3</LangVersion>
<PackageId>OpenFeature.Contrib.Provider.FeatureManagement</PackageId>
<VersionNumber>0.0.1-preview</VersionNumber>
<Version>$(VersionNumber)</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" />
<!--<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />-->
</ItemGroup>

<ItemGroup>
<!--<PackageReference Update="OpenFeature" Version="1.3.1" />-->
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Xunit;

namespace OpenFeature.Contrib.Providers.FeatureManagement.Test
{
public class FeatureManagementProviderTestNoContext
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task MissingFlagKey_ShouldReturnDefault(bool defaultValue)
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.enabled.json")
.Build();

var provider = new FeatureManagementProvider(configuration);

// Act
var result = await provider.ResolveBooleanValue("MissingFlagKey", defaultValue);

// Assert
Assert.Equal(defaultValue, result.Value);
}

[Theory]
[InlineData("Flag_Boolean_AlwaysOn", true)]
[InlineData("Flag_Boolean_AlwaysOff", false)]
public async Task BooleanValue_ShouldReturnExpected(string key, bool expected)
{
// 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, !expected);

// Assert
Assert.Equal(expected, result.Value);
}

[Theory]
[InlineData("Flag_Double_AlwaysOn", 1.0)]
[InlineData("Flag_Double_AlwaysOff", -1.0)]
public async Task DoubleValue_ShouldReturnExpected(string key, double expected)
{
// 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, 0.0f);

// Assert
Assert.Equal(expected, result.Value);
}

[Theory]
[InlineData("Flag_Integer_AlwaysOn", 1)]
[InlineData("Flag_Integer_AlwaysOff", -1)]
public async Task IntegerValue_ShouldReturnExpected(string key, int expected)
{
// 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, 0);

// Assert
Assert.Equal(expected, result.Value);
}

[Theory]
[InlineData("Flag_String_AlwaysOn", "FlagEnabled")]
[InlineData("Flag_String_AlwaysOff", "FlagDisabled")]
public async Task StringValue_ShouldReturnExpected(string key, string expected)
{
// 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(expected, result.Value);
}

[Theory]
[InlineData("Flag_Structure_AlwaysOn")]
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 729632c

Please sign in to comment.