Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dependent feature toggles #168

Merged
merged 8 commits into from
Oct 17, 2023
56 changes: 52 additions & 4 deletions src/Unleash/DefaultUnleash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Unleash
using System.Linq;
using System.Threading;
using Unleash.Events;
using Unleash.Utilities;
using Unleash.Variants;

/// <inheritdoc />
Expand Down Expand Up @@ -38,6 +39,8 @@ public class DefaultUnleash : IUnleash

internal readonly UnleashServices services;

private readonly WarnOnce warnOnce;

///// <summary>
///// Initializes a new instance of Unleash client with a set of default strategies.
///// </summary>
Expand All @@ -59,6 +62,8 @@ public DefaultUnleash(UnleashSettings settings, bool overrideDefaultStrategies,

this.settings = settings;

warnOnce = new WarnOnce(Logger);

var settingsValidator = new UnleashSettingsValidator();
settingsValidator.Validate(settings);

Expand Down Expand Up @@ -100,7 +105,10 @@ public bool IsEnabled(string toggleName, UnleashContext context)

public bool IsEnabled(string toggleName, UnleashContext context, bool defaultSetting)
{
return CheckIsEnabled(toggleName, context, defaultSetting).Enabled;
var enabled = CheckIsEnabled(toggleName, context, defaultSetting).Enabled;
RegisterCount(toggleName, enabled);

return enabled;
}

private FeatureEvaluationResult CheckIsEnabled(
Expand All @@ -114,8 +122,6 @@ private FeatureEvaluationResult CheckIsEnabled(
var enabled = DetermineIsEnabledAndStrategy(toggleName, featureToggle, enhancedContext, defaultSetting, out var strategy);
var variant = DetermineVariant(enabled, featureToggle, strategy, enhancedContext, defaultVariant);

RegisterCount(toggleName, enabled);

if (featureToggle?.ImpressionData ?? false)
{
EmitImpressionEvent("isEnabled", enhancedContext, enabled, featureToggle.Name);
Expand Down Expand Up @@ -155,9 +161,44 @@ private bool DetermineIsEnabledAndStrategy(
GetStrategyOrUnknown(s.Name)
.IsEnabled(s.Parameters, enhancedContext, ResolveConstraints(s).Union(s.Constraints))
);
}

return strategy != null;
if (featureToggle.Dependencies.Any() && !ParentDependenciesAreSatisfied(featureToggle, enhancedContext))
{
return false;
}

return strategy != null;
}

private bool ParentDependenciesAreSatisfied(FeatureToggle featureToggle, UnleashContext context)
{
return featureToggle.Dependencies.All(d => DependenciesSatisfied(featureToggle, d, context));
}

private bool DependenciesSatisfied(FeatureToggle featureToggle, Dependency dependency, UnleashContext context)
{
var parentToggle = GetToggle(dependency.Feature);
if (parentToggle == null)
{
warnOnce.Warn(dependency.Feature + featureToggle.Name, $"UNLEASH: Parent feature toggle {dependency.Feature} was not found in the cache, the evaluation of this dependency will always be false");
return false;
}

if (parentToggle.Dependencies.Any()) {
return false;
}

if (dependency.Enabled) {
if (dependency.Variants != null && dependency.Variants.Any())
{
var checkResult = CheckIsEnabled(dependency.Feature, context, false, Variant.DISABLED_VARIANT);
return checkResult.Enabled && dependency.Variants.Contains(checkResult.Variant.Name);
}
return CheckIsEnabled(dependency.Feature, context, false).Enabled;
}

return !CheckIsEnabled(dependency.Feature, context, false).Enabled;
}

private Variant DetermineVariant(bool enabled,
Expand Down Expand Up @@ -195,12 +236,19 @@ public Variant GetVariant(string toggleName, Variant defaultVariant)
return GetVariant(toggleName, services.ContextProvider.Context, defaultVariant);
}

public Variant GetVariant(string toggleName, UnleashContext context)
{
return GetVariant(toggleName, context, Variant.DISABLED_VARIANT);
}

public Variant GetVariant(string toggleName, UnleashContext context, Variant defaultValue)
{
var toggle = GetToggle(toggleName);

var evaluationResult = CheckIsEnabled(toggleName, context, false, defaultValue);

RegisterCount(toggleName, evaluationResult.Enabled);

RegisterVariant(toggleName, evaluationResult.Variant);

var enhancedContext = context.ApplyStaticFields(settings);
Expand Down
24 changes: 24 additions & 0 deletions src/Unleash/Internal/Dependency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Unleash.Internal {
public class Dependency {
sighphyre marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Feature is the name of the feature toggle we depend upon
/// </summary>
public string Feature { get; }
/// <summary>
/// Variants contains a string of variants that the dependency should resolve to
/// </summary>
public string[] Variants { get; }
/// <summary>
/// Enabled is the property that determines whether the dependency should be on or off.
/// If the property is absent from the payload it's assumed to be default on
/// </summary>
public bool Enabled { get; }

public Dependency(string feature, string[] variants = null, bool? enabled = null)
{
Feature = feature;
Variants = variants ?? new string[0];
Enabled = enabled ?? true;
}
}
}
5 changes: 4 additions & 1 deletion src/Unleash/Internal/FeatureToggle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ namespace Unleash.Internal
{
public class FeatureToggle
{
public FeatureToggle(string name, string type, bool enabled, bool impressionData, List<ActivationStrategy> strategies, List<VariantDefinition> variants = null)
public FeatureToggle(string name, string type, bool enabled, bool impressionData, List<ActivationStrategy> strategies, List<VariantDefinition> variants = null, List<Dependency> dependencies = null)
{
Name = name;
Type = type;
Enabled = enabled;
ImpressionData = impressionData;
Strategies = strategies ?? new List<ActivationStrategy>();
Variants = variants ?? new List<VariantDefinition>();
Dependencies = dependencies ?? new List<Dependency>();
}

public string Name { get; }
Expand All @@ -24,6 +25,8 @@ public FeatureToggle(string name, string type, bool enabled, bool impressionData

public List<VariantDefinition> Variants { get; }

public List<Dependency> Dependencies { get; }

public override string ToString()
{
return $"FeatureToggle{{name=\'{Name}{'\''}, enabled={Enabled}, impressionData={ImpressionData}, strategies=\'{Strategies}{'\''}{'}'}";
Expand Down
2 changes: 1 addition & 1 deletion src/Unleash/Internal/UnleashServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class UnleashServices : IDisposable
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private readonly IUnleashScheduledTaskManager scheduledTaskManager;

const string supportedSpecVersion = "4.2.0";
const string supportedSpecVersion = "4.5.1";

internal CancellationToken CancellationToken { get; }
internal IUnleashContextProvider ContextProvider { get; }
Expand Down
29 changes: 29 additions & 0 deletions src/Unleash/Utilities/WarnOnce.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using Unleash.Logging;

namespace Unleash.Utilities
{
internal class WarnOnce
{
private readonly ILog logger;
private readonly HashSet<string> seen = new HashSet<string>();

public WarnOnce(ILog logger)
{
this.logger = logger;
}

public void Warn(string key, string message)
{
if (seen.Contains(key))
{
return;
}

seen.Add(key);
logger.Warn(message);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"version": 2,
"features": [
{
"name": "enabled-child",
"description": "Depends on parent",
"enabled": true,
"impressionData": false,
"strategies": [
{
"name": "default",
"parameters": {}
}
],
"dependencies": [
{
"feature": "enabled-parent"
}
]
},
{
"name": "enabled-parent",
"description": "A parent to depend upon",
"enabled": true,
"impressionData": false,
"strategies": [
{
"name": "default",
"parameters": {}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ static TestFactory()

using (var client = new HttpClient())
{
var csTestsVersion = "v4.3.3";
var csTestsVersion = "v4.5.2";
var indexPath = $"https://raw.githubusercontent.com/Unleash/client-specification/{csTestsVersion}/specifications/";
var indexResponse = client.GetStringAsync(indexPath + "index.json").Result;
var indexFilePath = Path.Combine(specificationsPath, "index.json");
Expand Down
Loading
Loading