diff --git a/ClassDiagram.png b/ClassDiagram.png
new file mode 100644
index 0000000..a83ad7a
Binary files /dev/null and b/ClassDiagram.png differ
diff --git a/README.md b/README.md
index c079cdd..082fd47 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://badge.fury.io/nu/FeatureOne) [](https://github.com/NinjaRocks/FeatureOne/blob/master/License.md) [](https://github.com/NinjaRocks/FeatureOne/actions/workflows/CI-Build.yml) [](https://github.com/ninjarocks/FeatureOne/releases/latest)
[](https://github.com/NinjaRocks/FeatureOne/actions/workflows/codeql.yml) [](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
-FeatureOne is a .Net Library to implement feature toggles.
+.Net Library to implement feature toggles.
--
### What is a feature toggle?
@@ -18,24 +18,30 @@ FeatureOne is a .Net Library to implement feature toggles.
How to use FeatureOne
--
-Step 1. In order to release a new functionality or feature - say eg. Dashboard Widget.
+### Step 1.
+In order to release a new functionality or feature - say eg. Dashboard Widget.
Add logical check in codebase to wrap the functionality under a `feature toggle`.
-> the logical check is evaluated at runtime against the status of toggle from store provider.
+> the logical check evaluates status of the toggle configured for the feature in store at runtime.
```
-var featureName = "dashboard_widget"; // Name of functionality or feature to toggle.
-if(Features.IsEnable(featureName, claimsPrincipal){
+ var featureName = "dashboard_widget"; // Name of functionality or feature to toggle.
+ if(Features.IsEnable(featureName, claimsPrincipal){
showDashboardWidget();
}
```
-Step 2. Add toggles to the store (database or file or other medium) in order to conditionally enable/disable the feature. A `toggle` can consitute a collection of `conditions` that evaluate separately when the toggle runs. You can additionally specify an `operator` on the toggle to determine the overall success to include success of `any` constituent condition or success of `all` consituent conditions.
-> Toggles run at runtime based on consitituent conditions that evaluate seperaely against user claims (generally logged in user principal).
+
+### Step 2.
+Add `toggle` to the store (ie. a store in database or file or other medium) in order to conditionally enable/disable the feature.
+A toggle constitutes a collection of `conditions` that evaluate separately when the toggle is run. You can additionally specify an `operator` on the toggle to determine the overall success to include success of `any` constituent condition or success of `all` consituent conditions.
+> Toggles run at runtime based on consitituent conditions that evaluate separately against user claims (generally logged in user principal).
There are two types of conditions that can be used out of box for the toggles.
-- `Simple` condition - allows simple toggle to enable or disable of the feature
+#### i. Simple Condition
+`Simple` condition allows simple toggle to enable or disable of the feature. User claims are not taken into account for this condition.
+
Below is the serialized representation of feature toggle with simple condition to enable or disable a given feature.
```
{
@@ -49,16 +55,17 @@ Below is the serialized representation of feature toggle with simple condition t
}
}
```
+#### ii. Regex Condition
+`Regex` condition allows evaluating a user claim against a regex expression.
-- `Regex` condition - allows evaluating a user claim against a regex expression.
Below is the serialized representation of feature toggle with regex conditions to enable or disable a given feature.
```
-{
- "dashboard_widget":{ -- Feature name
+ {
+ "dashboard_widget":{ -- Feature name
"toggle"{ -- Toggle details for the feature
- "operator":"any|all", -- evalue overall toggle to true
- -- when any condition is met or all conditions are met.
+ "operator":"any|all", -- evaluate overall toggle to true
+ -- when `any` condition is met or `all` conditions are met.
"conditions":[{
"type":"Regex",
"claim":"email", -- email claim to be used for evaluation.
@@ -70,11 +77,12 @@ Below is the serialized representation of feature toggle with regex conditions t
expression":"*@gbk.com" -- Regex expression for evaulation.
}]
}
- }
-}
+ }
+ }
```
-Step 3. Implement `IStoreProvider` interface to return all configured feature toggles from custom store.
+### Step 3.
+Implement `IStoreProvider` interface to return all configured feature toggles from custom store.
Return a collection of key-value pairs with key mapping to `featureName` and value mapping to string representation of json serialized `toggle`
```
///
@@ -95,28 +103,128 @@ Return a collection of key-value pairs with key mapping to `featureName` and val
/// "type":"Regex",
/// "claim":"email",
/// "expression":"*@gbk.com"
- /// },
- /// {
+ /// },
+ /// {
/// "type":"RegexCondition",
/// "claim":"user_role",
/// "expression":"^administrator$"
/// }]
/// }
- ///
- ///
+ ///
+ /// KeyValuePair Array
IEnumerable> Get();
}
```
-Step 4. Example - IoC Container Registrations
-```
-TBC ....
+### Step 4.
+Example - IoC Container Registrations
```
+ services.UseFeatureOne(new Configuration
+ {
+ // Optional logger implementation
+ Logger = new CustomLoggerImpl(),
+ // Custom store provider implementation.
+ StoreProvider = new SQlStoreProviderImpl()
+ }
+```
+How to Extend FeatureOne
+--
+### Toggle Condition
+You could add your own conditions by extending the `ICondtion` interface. The interface provides `evaluate()` method that returns a boolean as a result of evaluating logic against list of input claims.
+```
+ ///
+ /// Interface to implement toggle condition.
+ ///
+ public interface ICondition
+ {
+ ///
+ /// Implement method to evaulate toggle condition.
+ ///
+ /// List of user claims; could be null
+ ///
+ bool Evaluate(IDictionary claims);
+ }
+```
+`Please Note` The condition class should only include primitive data type properties.
+Example below
+```
+ public class TimeCondition : ICondition
+ {
+ // toggle to show feature after given hour during the day.
+ public int Hour {get; set;} = 12; // Primitive int property.
+
+ bool Evaluate(IDictionary claims)
+ {
+ return (DateTime.Now.Hour > 12);
+ }
+ }
+
+ -- Example toggle to allow non-admin users access to a feature only after 14 hrs.
+
+ {
+ operator":"any",
+ "conditions":[{
+ "type":"Regex",
+ "claim":"user_role",
+ "expression":"^administrator$"
+ },
+ {
+ "type":"Time",
+ "Hour":14
+ }]
+ }
+```
+### Store Provider
+To use FeatureOne, you need to provide implementation of `Store Provider` to get all the feature toggles from storage medium of choice. Implement `IStoreProvider` interface to return the key-value pairs with feature name and json string toggle.
+Below is an example of dummy provider implementation.
+> A production implementation should be a provider with `API` or `SQL` or `File system` backend. Ideally, you may also use caching of feature toggles in the provider implementation to optimise calls to storage medium.
+```
+public class CustomStoreProvider : IStoreProvider
+ {
+ public IEnumerable> Get()
+ {
+ return new[] {
+ new KeyValuePair("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
+ new KeyValuePair("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}")
+ };
+ }
+ }
+```
+### Logger
+You could optionally provide an implementation of logger by wrapping your favourite logging libaray under `IFeatureLogger` interface. Please see the interface definition below. This implementation is optional and when no logger is provided FeatureOne will not log any errors.
+```
+ ///
+ /// Interface to implement custom logger.
+ ///
+ public interface IFeatureLogger
+ {
+ ///
+ /// Implement the debug log method
+ ///
+ /// log message
+ void Debug(string message);
+ ///
+ /// Implement the error log method
+ ///
+ /// log message
+ void Error(string message);
+ ///
+ /// Implement the info log method
+ ///
+ /// log message
+ void Info(string message);
+ ///
+ /// Implement the warn log method
+ ///
+ /// log message
+ void Warn(string message);
+ }
+```
\ No newline at end of file
diff --git a/src/FeatureOne/ClassDiagram.cd b/src/FeatureOne/ClassDiagram.cd
new file mode 100644
index 0000000..b17c4b9
--- /dev/null
+++ b/src/FeatureOne/ClassDiagram.cd
@@ -0,0 +1,154 @@
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAEAAAQAAAAAABAAAAAAAAAAAAA=
+ Core\Feature.cs
+
+
+
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAEAAgAAAAAAAAAAAAAAAAAAAAA=
+ Core\Toggles\Conditions\SimpleCondition.cs
+
+
+
+
+
+
+ AAAAIAAAAAAAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAA=
+ Core\Toggles\Conditions\RegexCondition.cs
+
+
+
+
+
+
+ AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAIAI=
+ Core\Toggle.cs
+
+
+
+
+
+
+
+
+
+ AgAAAAAAAAAAAQAAAAGAAAAAAAAAAAAAgAAAAAAAAAA=
+ Features.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ EAAAAAAAAAAAAQAAAAAAAAAAAAAgAAAAggAAAAAAAAA=
+ Core\Stores\FeatureStore.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AAAAAAAAAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA=
+ Configuration.cs
+
+
+
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=
+ Core\Stores\ToggleDeserializer.cs
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAA=
+ Core\ICondition.cs
+
+
+
+
+
+ AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAIAI=
+ Core\IToggle.cs
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAEAAAQAAAAAABAAAAAAAAAAAAA=
+ IFeature.cs
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAgAAAAAAAAA=
+ IFeatureStore.cs
+
+
+
+
+
+ AAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+ Core\Stores\IStoreProvider.cs
+
+
+
+
+
+ AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=
+ Core\Stores\IToggleDeserializer.cs
+
+
+
+
+
+ AAAAAAAAAAAAAYAAAAAAAAAAAAAAAAEAAAAAAEAAAAA=
+ IFeatureLogger.cs
+
+
+
+
\ No newline at end of file
diff --git a/src/FeatureOne/Configuration.cs b/src/FeatureOne/Configuration.cs
new file mode 100644
index 0000000..0b31a5d
--- /dev/null
+++ b/src/FeatureOne/Configuration.cs
@@ -0,0 +1,10 @@
+using FeatureOne.Core.Stores;
+
+namespace FeatureOne
+{
+ public class Configuration
+ {
+ public IFeatureLogger Logger { get; set; } = new NullLogger();
+ public IStoreProvider StoreProvider { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/FeatureStore.cs b/src/FeatureOne/Core/Stores/FeatureStore.cs
index 7b482d3..d309b46 100644
--- a/src/FeatureOne/Core/Stores/FeatureStore.cs
+++ b/src/FeatureOne/Core/Stores/FeatureStore.cs
@@ -3,18 +3,18 @@
public class FeatureStore : IFeatureStore
{
private IStoreProvider storeProvider;
- private readonly FeatureConfiguration Configuration;
+ private readonly Configuration Configuration;
private IToggleDeserializer toggleDeserializer;
- public FeatureStore(IStoreProvider storeProvider) : this(storeProvider, new ToggleDeserializer(), new FeatureConfiguration())
+ public FeatureStore(IStoreProvider storeProvider) : this(storeProvider, new ToggleDeserializer(), new Configuration())
{
}
- public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer) : this(storeProvider, toggleDeserializer, new FeatureConfiguration())
+ public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer) : this(storeProvider, toggleDeserializer, new Configuration())
{
}
- public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer, FeatureConfiguration configuration)
+ public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer, Configuration configuration)
{
this.storeProvider = storeProvider;
Configuration = configuration;
@@ -37,11 +37,11 @@ public IEnumerable GetAll()
{
try
{
- result.Add(new Feature(feature.Name, toggleDeserializer.Deserializer(feature.Toggle)));
+ result.Add(new Feature(feature.Key, toggleDeserializer.Deserializer(feature.Value)));
}
catch (Exception ex)
{
- Configuration.Logger?.Error($"Action='Failed to Deserialize', Feature='{feature.Name}', Exception='{ex}'.");
+ Configuration.Logger?.Error($"Action='Failed to Deserialize', Feature='{feature.Key}', Exception='{ex}'.");
}
}
diff --git a/src/FeatureOne/Core/Stores/IStoreProvider.cs b/src/FeatureOne/Core/Stores/IStoreProvider.cs
index 0fba2bf..3586807 100644
--- a/src/FeatureOne/Core/Stores/IStoreProvider.cs
+++ b/src/FeatureOne/Core/Stores/IStoreProvider.cs
@@ -10,8 +10,8 @@ public interface IStoreProvider
///
///
/// Example:
- /// Name - Feature-01
- /// Toggle - Json string as
+ /// Key - Feature-01
+ /// Value - Json string as
/// {
/// "operator":"all",
/// "conditions":[{
@@ -27,18 +27,6 @@ public interface IStoreProvider
/// }
///
///
- FeatureRecord[] Get();
- }
-
- public class FeatureRecord
- {
- public FeatureRecord(string name, string toggle)
- {
- Name = name;
- Toggle = toggle;
- }
-
- public string Name { get; set; }
- public string Toggle { get; set; }
+ IEnumerable> Get();
}
}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/NullStoreProvder.cs b/src/FeatureOne/Core/Stores/NullStoreProvder.cs
new file mode 100644
index 0000000..a7de65a
--- /dev/null
+++ b/src/FeatureOne/Core/Stores/NullStoreProvder.cs
@@ -0,0 +1,10 @@
+namespace FeatureOne.Core.Stores
+{
+ public class NullStoreProvder : IStoreProvider
+ {
+ public IEnumerable> Get()
+ {
+ return Enumerable.Empty>();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne/FeatureConfiguration.cs b/src/FeatureOne/FeatureConfiguration.cs
deleted file mode 100644
index c019d09..0000000
--- a/src/FeatureOne/FeatureConfiguration.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace FeatureOne
-{
- public class FeatureConfiguration
- {
- public bool UseCache { get; set; } = true;
- public TimeSpan SlidingExpiry { get; set; } = TimeSpan.FromMinutes(5);
- public IFeatureLogger Logger { get; set; } = new NullLogger();
- }
-}
\ No newline at end of file
diff --git a/src/FeatureOne/FeatureOne.csproj b/src/FeatureOne/FeatureOne.csproj
index 3302866..88f8326 100644
--- a/src/FeatureOne/FeatureOne.csproj
+++ b/src/FeatureOne/FeatureOne.csproj
@@ -42,4 +42,8 @@
+
+
+
+
diff --git a/src/FeatureOne/Features.cs b/src/FeatureOne/Features.cs
index 1045013..320c17d 100644
--- a/src/FeatureOne/Features.cs
+++ b/src/FeatureOne/Features.cs
@@ -8,17 +8,16 @@ namespace FeatureOne
public class Features
{
private readonly IFeatureStore featureStore;
- private readonly FeatureConfiguration Configuration;
+ private readonly Configuration Configuration;
public static Features Current { get; private set; }
- public Features(IFeatureStore featureStore) : this(featureStore, new FeatureConfiguration
+ public Features(IFeatureStore featureStore) : this(featureStore, new Configuration
{
- UseCache = true,
Logger = new NullLogger()
})
{ }
- public Features(IFeatureStore featureStore, FeatureConfiguration configuration)
+ public Features(IFeatureStore featureStore, Configuration configuration)
{
this.featureStore = featureStore;
this.Configuration = configuration;
diff --git a/src/FeatureOne/IFeatureLogger.cs b/src/FeatureOne/IFeatureLogger.cs
index b3543c0..ac19b42 100644
--- a/src/FeatureOne/IFeatureLogger.cs
+++ b/src/FeatureOne/IFeatureLogger.cs
@@ -1,13 +1,29 @@
namespace FeatureOne
{
+ ///
+ /// Interface to implement custom logger.
+ ///
public interface IFeatureLogger
{
+ ///
+ /// Implement the debug log method
+ ///
+ /// log message
void Debug(string message);
-
+ ///
+ /// Implement the error log method
+ ///
+ /// log message
void Error(string message);
-
+ ///
+ /// Implement the info log method
+ ///
+ /// log message
void Info(string message);
-
+ ///
+ /// Implement the warn log method
+ ///
+ /// log message
void Warn(string message);
}
}
\ No newline at end of file
diff --git a/src/FeatureOne/ServiceRegistrations.cs b/src/FeatureOne/ServiceRegistrations.cs
new file mode 100644
index 0000000..9b256ac
--- /dev/null
+++ b/src/FeatureOne/ServiceRegistrations.cs
@@ -0,0 +1,34 @@
+using FeatureOne.Core.Stores;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FeatureOne
+{
+ public static class ServiceRegistrations
+ {
+ public static void UseFeatureOne(this ServiceCollection services, Configuration configuration)
+ {
+ if (configuration?.Logger == null)
+ services.AddTransient();
+
+ if (configuration?.StoreProvider == null)
+ services.AddTransient();
+
+ services.AddTransient();
+ services.AddTransient(c => configuration?.StoreProvider ?? c.GetService());
+
+ services.AddTransient(c =>
+ new FeatureStore(c.GetService(), c.GetService(), new Configuration
+ {
+ Logger = configuration?.Logger ?? c.GetService(),
+ StoreProvider = configuration?.StoreProvider ?? c.GetService()
+ }));
+
+ services.AddTransient(c =>
+ new Features(c.GetService(), new Configuration
+ {
+ Logger = configuration?.Logger ?? c.GetService(),
+ StoreProvider = configuration?.StoreProvider ?? c.GetService()
+ }));
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/FeatureOne.Tests/FeatureOne.Tests.csproj b/test/FeatureOne.Tests/FeatureOne.Tests.csproj
index 7a7aa3e..bfb3c1a 100644
--- a/test/FeatureOne.Tests/FeatureOne.Tests.csproj
+++ b/test/FeatureOne.Tests/FeatureOne.Tests.csproj
@@ -21,6 +21,10 @@
+
+
+
+
diff --git a/test/FeatureOne.Tests/FeaturesTests.cs b/test/FeatureOne.Tests/FeaturesTests.cs
index 818ede1..023032a 100644
--- a/test/FeatureOne.Tests/FeaturesTests.cs
+++ b/test/FeatureOne.Tests/FeaturesTests.cs
@@ -25,11 +25,9 @@ public void Setup()
store.Setup(x => x.FindStartsWith(It.IsAny())).Returns(new[] { feature.Object });
- features = new Features(store.Object, new FeatureConfiguration
+ features = new Features(store.Object, new Configuration
{
- Logger = logger.Object,
- SlidingExpiry = TimeSpan.FromSeconds(10),
- UseCache = true
+ Logger = logger.Object
});
principal = new ClaimsPrincipal(new ClaimsIdentity(new List
diff --git a/test/FeatureOne.Tests/Registeration/RegistrationTests.cs b/test/FeatureOne.Tests/Registeration/RegistrationTests.cs
new file mode 100644
index 0000000..ef28d55
--- /dev/null
+++ b/test/FeatureOne.Tests/Registeration/RegistrationTests.cs
@@ -0,0 +1,59 @@
+using Castle.DynamicProxy.Generators;
+using FeatureOne.Core.Stores;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace FeatureOne.Tests.Registeration
+{
+ [TestFixture]
+ internal class RegistrationTests
+ {
+ private Mock storeProvider;
+ private Mock logger;
+ private ServiceProvider serviceProvider;
+
+ [SetUp]
+ public void Setup() {
+
+ logger = new Mock();
+ storeProvider = new Mock();
+ storeProvider.Setup(x => x.Get())
+ .Returns(new[]
+ {
+ new KeyValuePair("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
+ new KeyValuePair("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}")
+ });
+
+ var services = new ServiceCollection();
+ services.UseFeatureOne(new Configuration
+ {
+ StoreProvider = storeProvider.Object,
+ Logger= logger.Object
+ });
+
+ serviceProvider = services.BuildServiceProvider();
+
+
+ }
+
+ [Test]
+ public void TestRegistration()
+ {
+ var features = serviceProvider.GetService();
+ var principal = new ClaimsPrincipal(new ClaimsIdentity(new List
+ {
+ new Claim("user", "ninja")
+ }));
+
+ var isEnabled = features.IsEnabled("feature-01", principal);
+
+ Assert.That(isEnabled, Is.True);
+ }
+ }
+}
diff --git a/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs b/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs
index a483e2c..daf2cba 100644
--- a/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs
+++ b/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs
@@ -21,11 +21,11 @@ public void Setup()
storeProvider.Setup(x => x.Get())
.Returns(new[]
{
- new FeatureRecord("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
- new FeatureRecord("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}")
+ new KeyValuePair("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
+ new KeyValuePair("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}")
});
- featureStore = new FeatureStore(storeProvider.Object, new ToggleDeserializer(), new FeatureConfiguration
+ featureStore = new FeatureStore(storeProvider.Object, new ToggleDeserializer(), new Configuration
{
Logger = logger.Object
});
@@ -71,8 +71,8 @@ public void TestGetAllToReturnAnyDeserializedFeaturesInStoreProvideAndLogErrorsF
storeProvider.Setup(x => x.Get())
.Returns(new[]
{
- new FeatureRecord("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
- new FeatureRecord("feature-02", "Invalid Toggle String")
+ new KeyValuePair("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
+ new KeyValuePair("feature-02", "Invalid Toggle String")
});
var features = featureStore.GetAll();
@@ -92,4 +92,15 @@ public void TestGetAllToReturnAnyDeserializedFeaturesInStoreProvideAndLogErrorsF
logger.Verify(x => x.Error(It.Is(msg => msg.Contains("feature-02"))), Times.Once());
}
}
+
+ public class CustomStoreProvider : IStoreProvider
+ {
+ public IEnumerable> Get()
+ {
+ return new[] {
+ new KeyValuePair("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
+ new KeyValuePair("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}")
+ };
+ }
+ }
}
\ No newline at end of file