Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added ClassDiagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
154 changes: 131 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![NuGet version](https://badge.fury.io/nu/FeatureOne.svg)](https://badge.fury.io/nu/FeatureOne) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/NinjaRocks/FeatureOne/blob/master/License.md) [![CI](https://github.com/NinjaRocks/FeatureOne/actions/workflows/CI-Build.yml/badge.svg)](https://github.com/NinjaRocks/FeatureOne/actions/workflows/CI-Build.yml) [![GitHub Release](https://img.shields.io/github/v/release/ninjarocks/FeatureOne?logo=github&sort=semver)](https://github.com/ninjarocks/FeatureOne/releases/latest)
[![CodeQL](https://github.com/NinjaRocks/FeatureOne/actions/workflows/codeql.yml/badge.svg)](https://github.com/NinjaRocks/FeatureOne/actions/workflows/codeql.yml) [![.Net Stardard](https://img.shields.io/badge/.Net%20Standard-6.0-blue)](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?
Expand All @@ -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.
```
{
Expand All @@ -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.
Expand All @@ -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`
```
/// <summary>
Expand All @@ -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$"
/// }]
/// }
/// </remarks>
/// <returns></returns>
/// </remarks>
/// <returns>KeyValuePair Array</returns>
IEnumerable<KeyValuePair<string, string>> 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.
```
/// <summary>
/// Interface to implement toggle condition.
/// </summary>
public interface ICondition
{
/// <summary>
/// Implement method to evaulate toggle condition.
/// </summary>
/// <param name="claims">List of user claims; could be null</param>
/// <returns></returns>
bool Evaluate(IDictionary<string, string> 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<string, string> 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<KeyValuePair<string, string>> Get()
{
return new[] {
new KeyValuePair<string, string>("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"),
new KeyValuePair<string, string>("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.
```
/// <summary>
/// Interface to implement custom logger.
/// </summary>
public interface IFeatureLogger
{
/// <summary>
/// Implement the debug log method
/// </summary>
/// <param name="message">log message</param>
void Debug(string message);

/// <summary>
/// Implement the error log method
/// </summary>
/// <param name="message">log message</param>
void Error(string message);

/// <summary>
/// Implement the info log method
/// </summary>
/// <param name="message">log message</param>
void Info(string message);

/// <summary>
/// Implement the warn log method
/// </summary>
/// <param name="message">log message</param>
void Warn(string message);
}
```
154 changes: 154 additions & 0 deletions src/FeatureOne/ClassDiagram.cd
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<ClassDiagram MajorVersion="1" MinorVersion="1">
<Class Name="FeatureOne.Core.Feature">
<Position X="5.5" Y="2.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAEAAAQAAAAAABAAAAAAAAAAAAA=</HashCode>
<FileName>Core\Feature.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Toggle" />
</ShowAsAssociation>
</Class>
<Class Name="FeatureOne.Core.Toggles.Conditions.SimpleCondition">
<Position X="12" Y="4.75" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAEAAgAAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Core\Toggles\Conditions\SimpleCondition.cs</FileName>
</TypeIdentifier>
<Lollipop Position="0.638" />
</Class>
<Class Name="FeatureOne.Core.Toggles.Conditions.RegexCondition">
<Position X="10.25" Y="4.75" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAIAAAAAAAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAA=</HashCode>
<FileName>Core\Toggles\Conditions\RegexCondition.cs</FileName>
</TypeIdentifier>
<Lollipop Position="0.61" />
</Class>
<Class Name="FeatureOne.Core.Toggle">
<Position X="8.25" Y="3.25" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAIAI=</HashCode>
<FileName>Core\Toggle.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Conditions" />
</ShowAsAssociation>
<Lollipop Position="0.7" />
</Class>
<Class Name="FeatureOne.Features">
<Position X="4.25" Y="7.75" Width="1.5" />
<TypeIdentifier>
<HashCode>AgAAAAAAAAAAAQAAAAGAAAAAAAAAAAAAgAAAAAAAAAA=</HashCode>
<FileName>Features.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Field Name="featureStore" />
<Field Name="Configuration" />
</ShowAsAssociation>
</Class>
<Class Name="FeatureOne.Core.Stores.FeatureStore">
<Position X="7" Y="8.5" Width="1.75" />
<AssociationLine Name="Configuration" Type="FeatureOne.Configuration" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
<Path>
<Point X="8.75" Y="8.938" />
<Point X="9.705" Y="8.938" />
<Point X="9.705" Y="6.145" />
<Point X="8.5" Y="6.145" />
</Path>
</AssociationLine>
<AssociationLine Name="toggleDeserializer" Type="FeatureOne.Core.Stores.IToggleDeserializer" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
<Path>
<Point X="7.875" Y="9.988" />
<Point X="7.875" Y="10.622" />
<Point X="7.875" Y="10.622" />
<Point X="7.875" Y="10.75" />
</Path>
</AssociationLine>
<TypeIdentifier>
<HashCode>EAAAAAAAAAAAAQAAAAAAAAAAAAAgAAAAggAAAAAAAAA=</HashCode>
<FileName>Core\Stores\FeatureStore.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Field Name="storeProvider" />
<Field Name="Configuration" />
<Field Name="toggleDeserializer" />
</ShowAsAssociation>
<Lollipop Position="0.547" />
</Class>
<Class Name="FeatureOne.Configuration">
<Position X="7" Y="5.75" Width="1.5" />
<AssociationLine Name="Logger" Type="FeatureOne.IFeatureLogger" FixedFromPoint="true" FixedToPoint="true">
<Path>
<Point X="7.75" Y="5.75" />
<Point X="7.75" Y="5.375" />
<Point X="5.75" Y="5.375" />
</Path>
</AssociationLine>
<TypeIdentifier>
<HashCode>AAAAAAAAAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Configuration.cs</FileName>
</TypeIdentifier>
<ShowAsAssociation>
<Property Name="Logger" />
</ShowAsAssociation>
</Class>
<Class Name="FeatureOne.Core.Stores.ToggleDeserializer">
<Position X="7" Y="12.25" Width="1.75" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Core\Stores\ToggleDeserializer.cs</FileName>
</TypeIdentifier>
</Class>
<Interface Name="FeatureOne.Core.ICondition">
<Position X="11" Y="3.25" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Core\ICondition.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.Core.IToggle" Collapsed="true">
<Position X="8.25" Y="2.25" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAIAI=</HashCode>
<FileName>Core\IToggle.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.IFeature" Collapsed="true">
<Position X="5.5" Y="1.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAEAAAQAAAAAABAAAAAAAAAAAAA=</HashCode>
<FileName>IFeature.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.IFeatureStore" Collapsed="true">
<Position X="7" Y="7.5" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAgAAAAAAAAA=</HashCode>
<FileName>IFeatureStore.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.Core.Stores.IStoreProvider">
<Position X="11.5" Y="8.75" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Core\Stores\IStoreProvider.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.Core.Stores.IToggleDeserializer">
<Position X="7" Y="10.75" Width="1.75" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=</HashCode>
<FileName>Core\Stores\IToggleDeserializer.cs</FileName>
</TypeIdentifier>
</Interface>
<Interface Name="FeatureOne.IFeatureLogger" Collapsed="true">
<Position X="4.25" Y="5.25" Width="1.5" />
<TypeIdentifier>
<HashCode>AAAAAAAAAAAAAYAAAAAAAAAAAAAAAAEAAAAAAEAAAAA=</HashCode>
<FileName>IFeatureLogger.cs</FileName>
</TypeIdentifier>
</Interface>
<Font Name="Segoe UI" Size="9" />
</ClassDiagram>
10 changes: 10 additions & 0 deletions src/FeatureOne/Configuration.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Loading