-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Eric Pattison <[email protected]>
- Loading branch information
1 parent
dd400df
commit 729632c
Showing
10 changed files
with
1,078 additions
and
37 deletions.
There are no files selected for viewing
9 changes: 0 additions & 9 deletions
9
src/OpenFeature.Contrib.Providers.FeatureManagement/Class1.cs
This file was deleted.
Oops, something went wrong.
174 changes: 174 additions & 0 deletions
174
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,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); | ||
} | ||
} | ||
} |
21 changes: 19 additions & 2 deletions
21
...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 |
---|---|---|
@@ -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> |
126 changes: 126 additions & 0 deletions
126
...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,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); | ||
} | ||
} | ||
} |
Oops, something went wrong.