diff --git a/GitVersion.yml b/GitVersion.yml
index cacc951..280505f 100644
--- a/GitVersion.yml
+++ b/GitVersion.yml
@@ -1,4 +1,4 @@
-next-version: 1.0.6
+next-version: 2.0.0
tag-prefix: '[vV]'
mode: ContinuousDeployment
branches:
diff --git a/Ninja.FeatureOne.sln b/Ninja.FeatureOne.sln
index 941ec11..570903b 100644
--- a/Ninja.FeatureOne.sln
+++ b/Ninja.FeatureOne.sln
@@ -25,6 +25,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOne", "src\FeatureOn
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOne.Tests", "test\FeatureOne.Tests\FeatureOne.Tests.csproj", "{F51B62E7-1A8F-41DC-B027-074932A01D33}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureOne.SQL", "src\FeatureOne.SQL\FeatureOne.SQL.csproj", "{FED48C4A-1EEC-491C-8F5D-886763DF9388}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureOne.SQL.Tests", "test\FeatureOne.SQL.Tests\FeatureOne.SQL.Tests.csproj", "{A6102616-CA8C-4C7F-AF16-C8F451E6D62D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,6 +43,14 @@ Global
{F51B62E7-1A8F-41DC-B027-074932A01D33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F51B62E7-1A8F-41DC-B027-074932A01D33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F51B62E7-1A8F-41DC-B027-074932A01D33}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FED48C4A-1EEC-491C-8F5D-886763DF9388}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FED48C4A-1EEC-491C-8F5D-886763DF9388}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FED48C4A-1EEC-491C-8F5D-886763DF9388}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FED48C4A-1EEC-491C-8F5D-886763DF9388}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A6102616-CA8C-4C7F-AF16-C8F451E6D62D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A6102616-CA8C-4C7F-AF16-C8F451E6D62D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6102616-CA8C-4C7F-AF16-C8F451E6D62D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A6102616-CA8C-4C7F-AF16-C8F451E6D62D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -47,6 +59,8 @@ Global
{FB8FCDD0-A3D4-4776-85E0-C47ECFB3D310} = {9930EAFC-568F-4676-82AB-2B8F9D7535B3}
{65BD1F94-CEF4-4CC1-8482-3FF72BBF620F} = {B5EFC87D-E5C5-488E-AA89-06D26027E4C7}
{F51B62E7-1A8F-41DC-B027-074932A01D33} = {E864826B-B5D2-4DAD-B53A-2A3976226B53}
+ {FED48C4A-1EEC-491C-8F5D-886763DF9388} = {B5EFC87D-E5C5-488E-AA89-06D26027E4C7}
+ {A6102616-CA8C-4C7F-AF16-C8F451E6D62D} = {E864826B-B5D2-4DAD-B53A-2A3976226B53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {745CC402-145C-4E31-BAB7-DA93F8292512}
diff --git a/README.md b/README.md
index 58d2d34..88f9ef5 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,14 @@
-#
FeatureOne v1.0.7
+#
FeatureOne v2.0.0
[](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/2.1)
.Net Library to implement feature toggles.
--
-
+> #### Nuget Packages
+> ---
+> `FeatureOne` - Provides core funtionality to implement feature toggles with `no` backend storage provider. Needs package consumer to provide `IStorageProvider` implementation. Ideal for use case that requires custom storage backend. Please see below for more details.
+>
+>`FeatureOne.SQL` - Provides FeatureOne funtionality to implement feature toggles using `SQL` Storage backend.
### What is a feature toggle?
> Feature toggle is a mechanism that allows code to be turned “on” or “off” remotely without the need for a deploy. Feature toggles are commonly used in applications to gradually roll out new features, allowing teams to test changes on a small subset of users before releasing them to everyone.
@@ -25,7 +29,7 @@ Add logical check in codebase to wrap the functionality under a `feature toggle`
```
var featureName = "dashboard_widget"; // Name of functionality or feature to toggle.
- if(Features.Current.IsEnable(featureName, claimsPrincipal){ // See other IsEnable() overloads
+ if(Features.Current.IsEnable(featureName){ // See other IsEnable() overloads
showDashboardWidget();
}
```
@@ -36,14 +40,15 @@ Add a `toggle` definition to storage ie. a store in database or file or other st
A toggle constitutes a collection of `conditions` that evaluate separately when the toggle is run. You can additionally specify an `operator` in the toggle definition 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).
-JSON Syntax for Feature Toggle is below
+Below is a serialized JSON representation of a Feature Toggle.
```
{
"feature_name":{ -- Feature name
- "toggle":{ -- Toggle details for the feature
+ "toggle":{ -- Toggle definition for the feature
- "operator":"any|all", -- Evaluate overall toggle to true
- -- when `any` condition is met or `all` conditions are met.
+ "operator":"any|all", -- Logical Operator - any (OR) & all (AND)
+ -- ie. Evaluate overall toggle to true when `any` condition is met or
+ -- `all` conditions are met.
"conditions":[{ -- collection of conditions
"type":"simple|regex" -- type of condition
@@ -54,6 +59,7 @@ JSON Syntax for Feature Toggle is below
}
}
```
+
### Condition Types
There are two types of toggle conditions that can be used out of box.
@@ -64,7 +70,7 @@ Below is the serialized representation of toggle with simple condition.
```
{
"dashboard_widget":{
- "toggle"{
+ "toggle":{
"conditions":[{
"type":"Simple", -- Simple Condition.
"isEnabled":true|false -- Enabled or disable the feature.
@@ -73,57 +79,91 @@ Below is the serialized representation of toggle with simple condition.
}
}
```
+C# representation of a feature with simple toggle is
+```
+var feature = new Feature
+{
+ Name ="dashboard_widget", // Feature Name
+ Toggle = new Toggle // Toggle definition
+ {
+ // Logical operator to be applied when evaluating consituent conditions.
+ Operator = Operator.Any, // Default is Any (Logical OR)
+
+ Conditions = new[]
+ {
+ // Simple condition that can be set to true/false for feature to be enabled/disabled.
+ new SimpleCondition { IsEnabled = true }
+ }
+ }
+}
+```
#### ii. Regex Condition
-`Regex` condition allows evaluating a regex expression against specified user claim to enable a given feature.
+`Regex` condition allows evaluating a regex expression against specified user claim value to enable a given feature.
Below is the serialized representation of toggle with regex condition.
```
{
"dashboard_widget":{
- "toggle"{
+ "toggle":{
"conditions":[{
"type":"Regex", -- Regex Condition
"claim":"email", -- Claim 'email' to be used for evaluation.
- "expression":"*@gbk.com" -- Regex expression to be used for evaulation.
+ "expression":"*@gbk.com" -- Regex expression to be used for evaluation.
}]
}
}
}
```
-
-### Step 3. Provide Storage Provider Implementation.
-To use FeatureOne, you need to provide implementation of `Storage Provider` to get all the feature toggles from storage medium of choice.
-Implement `IStorageProvider` interface to get configured feature toggles from storage.
-The interface has `Get()` method that returns a collection of `KeyValuePair` with `key` mapping to `featureName` and `value` mapping to json string representation of the `toggle`
+C# representation of a feature with regex toggle is
```
- ///
- /// Interface to implement storage provider.
- ///
- public interface IStorageProvider
+
+var feature = new Feature
+{
+ Name ="dashboard_widget", // Feature Name
+ Toggle = new Toggle // Toggle definition
+ {
+ Operator = Operator.Any,
+ Conditions = new[]
{
+ // Regex condition that evalues role of user to be administrator to enable the feature.
+ new RegexCondition { Claim = "role", Expression = "administrator" }
+ }
+ }
+}
+```
+
+### Step 3. Implement Storage Provider.
+To use FeatureOne, you need to provide implementation for `Storage Provider` to get all the feature toggles from storage medium of choice.
+Implement `IStorageProvider` interface to return feature toggles from storage.
+The interface has `GetByName()` method that returns an array of `IFeature`
+```
+ ///
+ /// Interface to implement storage provider.
+ ///
+ public interface IStorageProvider
+ {
///
- /// Implement this method to get all feature toggles from storage.
+ /// Implement to get storage feature toggles by a given name.
///
- ///
- /// Example:
- /// Key - "dashboard_widget"
- /// Value - "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"
- ///
- /// KeyValuePair Array
- IEnumerable> Get();
- }
+ /// Array of Features
+ IFeature[] GetByName(string name);
+ }
```
+A production storage provider should be an implementation with `API` , `SQL` or `File system` storage backend.
+
+An implementation option is to store features as serialized json to backend medium. Ideally, you may also want to use `caching` in the production implementation to optimise calls to the storage backend.
+
+
Below is an example of dummy provider implementation.
-> A production provider should be an implementation with `API` , `SQL` or `File system` storage backend. Ideally, you may also want to use `caching` of feature toggles in the production implementation to optimise calls to the storage medium.
```
public class CustomStoreProvider : IStorageProvider
{
- public IEnumerable> Get()
+ public Feature[] GetByName(string name)
{
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\"}]}")
+ new Feature("feature-01",new Toggle(Operator.Any, new[]{ new SimpleCondition{IsEnabled=true}})),
+ new Feature("feature-02",new Toggle(Operator.All, new SimpleCondition { IsEnabled = false }, new RegexCondition{Claim="email", Expression= "*@gbk.com" }))
};
}
}
@@ -134,27 +174,18 @@ In bootstrap code, initialize the `Features` class with dependencies as shown be
i. With `storage provider` implementation.
```
- var storageProvider = new SQlStorageProviderImpl();
+ var storageProvider = new CustomStorageProviderImpl();
Features.Initialize(() => new Features(new FeatureStore(storageProvider)));
```
ii. With `storage provider` and `logger` implementations.
```
var logger = new CustomLoggerImpl();
- var storageProvider = new SQlStorageProviderImpl();
+ var storageProvider = new CustomStorageProviderImpl();
Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger));
```
-iii. With `storage provider`, `logger` and custom `toggle deserializer` implementations.
-```
- var logger = new CustomLoggerImpl();
- var storageProvider = new SQlStorageProviderImpl();
- var toggleDeserializer = new CustomToggleDeserializerImpl();
-
- Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger, toggleDeserializer), logger));
-```
-
How to Extend FeatureOne
--
@@ -175,39 +206,59 @@ The interface provides `evaluate()` method that returns a boolean result of eval
bool Evaluate(IDictionary claims);
}
```
-`Please Note` The condition class should only include `primitive` data type `properties` for default deserialization. If you need to implement a much complex toggle condition with non-primitive properties then also provide custom implementation of `IToggleDeserializer` to support its deserialization along with other conditions.
-
Example below shows sample implementation of a custom condition.
```
// toggle condition to show feature after given hour during the day.
public class TimeCondition : ICondition
{
- public int Hour {get; set;} = 12; // Primitive int property.
+ public int Hour {get; set;} = 12;
- bool Evaluate(IDictionary claims)
+ public bool Evaluate(IDictionary claims)
{
return (DateTime.Now.Hour > Hour);
}
}
```
- Example usage of above condition in toggle to allow non-admin users access to a feature only after 14 hrs.
+ Example usage of above condition in toggle to allow non-admin users access to a feature only after 12 hrs.
+ C# representation of the feature is
+
+```
+var feature = new Feature
+{
+ Name ="feature_pen_test", // Feature Name
+ Toggle = new Toggle // Toggle definition
+ {
+ Operator = Operator.Any, // Enabled when one of below conditions are true.
+ Conditions = new[]
+ {
+ // Custom condition - allow access after 12 o'clock
+ new TimeCondition { Hour = 12 },
+ // Regex condition for allowing admin users by role claim.
+ new RegexCondition { Claim = "role", Expression = "^administrator$"}
+ }
+ }
+}
+```
+JSON Serialized representation is
```
{
+ "feature_pen_test":{
+ "toggle":{
"operator":"any", -- Any below condition evaluation to true should succeed the toggle.
"conditions":[{
- "type":"Time", -- Time condition to all access to all after 14hrs
+ "type":"Time", -- Time condition to allow access after 12 o'clock.
"Hour":14
},
{
"type":"Regex", -- Regex to allow admin access
- "claim":"user_role",
+ "claim":"role",
"expression":"^administrator$"
}]
+ }
}
```
-
### ii. Logger
You could optionally provide an implementation of a logger by wrapping your favourite logging libaray under `IFeatureLogger` interface.
Please see the interface definition below.
@@ -243,6 +294,91 @@ Please see the interface definition below.
void Warn(string message);
}
```
+## FeatureOne.SQL - Feature toggles with SQL Backend.
+In addition to all above offerings, the FeatureOne.SQL package provides out of box SQL storage provider.
+
+SQL support can easily be installed as a separate nuget package.
+```
+$ dotnet add package FeatureOne.SQL --version 2.0.0
+```
+Supports Db Providers `MSSQL: System.Data.SqlClient`, `ODBC: System.Data.Odbc`, `OLEDB: System.Data.OleDb`, `SQLite: System.Data.SQLite`, `MySQL: MySql.Data.MySqlClient` & `PostgreSQL: Npgsql`.
+
+
+
+For any other SQL provider, You need to add provider factory to `DbProviderFactories.RegisterFactory("ProviderName", ProviderFactory)` and pass the provider specific `connection settings` in SQLConfiguration.
+### Database Setup
+> Requires creating a feature table with columns for feature name, toggle definition and feature archival.
+
+SQL SCRIPT below.
+```
+CREATE TABLE TFeatures (
+ Id INT NOT NULL IDENTITY PRIMARY KEY,
+ Name VARCHAR(255) NOT NULL,
+ Toggle NVARCHAR(4000) NOT NULL,
+ Archived BIT CONSTRAINT DF_TFeatures_Archived DEFAULT (0)
+);
+```
+
+### Example Table Record
+> Feature toggles need to be `scripted` to backend database in JSON format.
+
+Please see example entries below.
+
+| Name |Toggle | Archived |
+||||
+| dashboard_widget |{ "conditions":[{ "type":"Simple", "isEnabled": true }] } | false |
+|pen_test_dashboard| { "operator":"any", "conditions":[{ "type":"simple", "isEnabled":false}, { "type":"Regex", "claim":"email","expression":"^[a-zA-Z0-9_.+-]+@gbk.com" }]} | false|
+
+`Please Note` Any custom condition implementation should only include `primitive type` properties to work with `default` ICondition `deserialization`. When you need to implement a much complex toggle condition with `non-primitive` properties then you need to provide `custom` implementation of `IConditionDeserializer` to support its deserialization to toggle condition object.
+
+### Bootstrap initialization
+> See below bootstrap initialization for FeatureOne with SQL backend.
+#### SQL Configuration - Set connection string and other settings.
+```
+ var sqlConfiguration = new SQLConfiguration
+ {
+ ConnectionSettings = new ConnectionSettings
+ {
+ Providername = DbProviderName.MSSql, // provider Name for DbFactory registration.
+ ConnectionString ="Data Source=Powerstation; Initial Catalog=Features; Integrated Security=SSPI;"
+ },
+ FeatureTable = new FeatureTable
+ {
+ TableName = "[Features].[dbo].[TFeatures]", // Table and column name overrides.
+ NameColumn = "[Name]",
+ ToggleColumn = "[Toggle]",
+ ArchivedColumn = "[Archived]"
+ },
+ CacheSettings = new CacheSettings
+ {
+ EnableCache = true, // Enable cache with absolute expiry in Minutes.
+ ExpiryInMinutes = 60
+ }
+ }
+```
+i. With SQL configuration.
+```
+ var storageProvider = new SQlStorageProvider(sqlConfiguration);
+ Features.Initialize(() => new Features(new FeatureStore(storageProvider)));
+```
+ii. With Custom logger implementation, default is no logger.
+```
+ var logger = new CustomLoggerImpl();
+ var storageProvider = new SQlStorageProvider(sqlConfiguration, logger);
+
+ Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger));
+```
+
+iii. With other overloads - Custom cache and Toggle Condition deserializer.
+```
+ var toggleConditionDeserializer = CustomConditionDeserializerImpl(); // Implements IConditionDeserializer
+ var featureCache = CustomFeatureCache(); // Implements ICache
+
+ var storageProvider = new SQlStorageProvider(sqlConfiguration, logger, featureCache, toggleConditionDeserializer);
+
+ Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger));
+```
+
Credits
--
Thank you for reading. Please fork, explore, contribute and report. Happy Coding !! :)
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 0000000..88b63ad
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1 @@
+markdown: GFM
diff --git a/src/FeatureOne/Core/Stores/ConditionFactory.cs b/src/FeatureOne.SQL/ConditionDeserializer.cs
similarity index 79%
rename from src/FeatureOne/Core/Stores/ConditionFactory.cs
rename to src/FeatureOne.SQL/ConditionDeserializer.cs
index c0aca26..596fc4c 100644
--- a/src/FeatureOne/Core/Stores/ConditionFactory.cs
+++ b/src/FeatureOne.SQL/ConditionDeserializer.cs
@@ -5,10 +5,11 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
+using FeatureOne.Core;
-namespace FeatureOne.Core.Stores
+namespace FeatureOne.SQL
{
- public static class ConditionFactory
+ internal class ConditionDeserializer : IConditionDeserializer
{
private static Type[] loaddedTypes;
@@ -17,32 +18,36 @@ private static Type[] LoaddedTypes
get
{
if (loaddedTypes == null || loaddedTypes.Length == 0)
- loaddedTypes = Assembly.GetExecutingAssembly().GetTypes();
+ loaddedTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes())
+ .Where(p => typeof(ICondition).IsAssignableFrom(p)).ToArray();
return loaddedTypes;
}
}
- public static ICondition Create(JsonObject JsonObject)
+ public ICondition Deserialize(JsonObject condition)
{
- if (JsonObject == null)
- throw new ArgumentNullException(nameof(JsonObject));
+ if (condition == null)
+ throw new ArgumentNullException(nameof(condition));
- var typeName = JsonObject?["type"]?.ToString();
+ var typeName = condition?["type"]?.ToString();
var toggle = CreateInstance(new NamePostFix(typeName, "Condition"));
- HydrateToggle(toggle, JsonObject);
+ HydrateToggle(toggle, condition);
return toggle;
}
- public static ICondition[] Create(JsonObject[] conditions)
+ private static ICondition CreateInstance(NamePostFix conditionName)
{
- if (conditions == null)
- throw new ArgumentNullException(nameof(conditions));
+ var type = LoaddedTypes
+ .FirstOrDefault(p => typeof(ICondition).IsAssignableFrom(p) && p.Name.Equals(conditionName.Name, StringComparison.OrdinalIgnoreCase));
- return conditions.Select(s => Create(s)).ToArray();
+ if (type == null)
+ throw new Exception($"Could not find a toggle type for: '{conditionName.Name}'");
+
+ return (ICondition)Activator.CreateInstance(type, true);
}
private static void HydrateToggle(ICondition toggleCondition, JsonObject state)
@@ -75,16 +80,5 @@ private static PropertyInfo[] GetProperties(ICondition condition)
return propertyInfos;
}
-
- private static ICondition CreateInstance(NamePostFix conditionName)
- {
- var type = LoaddedTypes
- .FirstOrDefault(p => typeof(ICondition).IsAssignableFrom(p) && p.Name.Equals(conditionName.Name, StringComparison.OrdinalIgnoreCase));
-
- if (type == null)
- throw new Exception($"Could not find a toggle type for: '{conditionName.Name}'");
-
- return (ICondition)Activator.CreateInstance(type, true);
- }
}
}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/DbRecord.cs b/src/FeatureOne.SQL/DbRecord.cs
new file mode 100644
index 0000000..c6a64f3
--- /dev/null
+++ b/src/FeatureOne.SQL/DbRecord.cs
@@ -0,0 +1,8 @@
+namespace FeatureOne.SQL
+{
+ public class DbRecord
+ {
+ public string Name { get; set; }
+ public string Toggle { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/FeatureCache.cs b/src/FeatureOne.SQL/FeatureCache.cs
new file mode 100644
index 0000000..aa07ee0
--- /dev/null
+++ b/src/FeatureOne.SQL/FeatureCache.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Runtime.Caching;
+
+namespace FeatureOne.SQL
+{
+ internal class FeatureCache : ICache
+ {
+ public void Add(string key, object value, int expiry)
+ => MemoryCache.Default.Add(key, value, new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(expiry) });
+
+ public object Get(string key) => MemoryCache.Default.Get(key);
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/FeatureOne.SQL.csproj b/src/FeatureOne.SQL/FeatureOne.SQL.csproj
new file mode 100644
index 0000000..cce9617
--- /dev/null
+++ b/src/FeatureOne.SQL/FeatureOne.SQL.csproj
@@ -0,0 +1,73 @@
+
+
+
+ netstandard2.1
+ disable
+ disable
+ True
+ False
+ AssemblyInfo.cs
+ true
+ true
+ FeatureOne.SQL
+ FeatureOne.SQL
+ False
+ snupkg
+ FeatureOne.SQL
+ ninja.shayk
+ Ninja.Sha!4H
+ FeatureOne
+ .Net library to implement feature toggles with SQL backend.
+ Copyright (c) 2023 Ninja Sha!4h
+ README.md
+ https://github.com/NinjaRocks/FeatureOne
+ git
+ feature-toggle; feature-flag; feature-flags; feature-toggles; .netstandard2.1; featureOne; SQL-Backend; SQL-Toggles; SQL
+ 2.0.0
+ License.md
+ ninja-icon-16.png
+
+ Release Notes v2.0.0. - SQL Storage Provider
+ Core Functionality :-
+ Added support for SQL storage provider for Feature Toggle.
+ - Supports MSSQL, SQLite, ODBC, OLEDB, MySQL, PostgreSQL Db providers.
+ - Provides memory caching - enabled via configuration.
+ - Added extensibility for Custom implementations-
+ - Provides extension point to support more SQL providers.
+ - Provides extenion point for custom caching.
+ - Provides extension point for custom deserializer for Toggle Conditions.
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/FeatureOne.SQL/ICache.cs b/src/FeatureOne.SQL/ICache.cs
new file mode 100644
index 0000000..8119e31
--- /dev/null
+++ b/src/FeatureOne.SQL/ICache.cs
@@ -0,0 +1,20 @@
+namespace FeatureOne.SQL
+{
+ public interface ICache
+ {
+ ///
+ /// Add item to cache
+ ///
+ ///
+ ///
+ ///
+ void Add(string key, object value, int expiry);
+
+ ///
+ /// Get item from cache
+ ///
+ ///
+ ///
+ object Get(string key);
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/IConditionDeserializer.cs b/src/FeatureOne.SQL/IConditionDeserializer.cs
new file mode 100644
index 0000000..f612504
--- /dev/null
+++ b/src/FeatureOne.SQL/IConditionDeserializer.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Nodes;
+using FeatureOne.Core;
+
+namespace FeatureOne.SQL
+{
+ ///
+ /// Implement to provide deserialization strategy of condition types.
+ ///
+ public interface IConditionDeserializer
+ {
+ ICondition Deserialize(JsonObject condition);
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/IDbRepository.cs b/src/FeatureOne.SQL/IDbRepository.cs
new file mode 100644
index 0000000..0d0b8f9
--- /dev/null
+++ b/src/FeatureOne.SQL/IDbRepository.cs
@@ -0,0 +1,7 @@
+namespace FeatureOne.SQL
+{
+ public interface IDbRepository
+ {
+ DbRecord[] GetByName(string name);
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/IToggleDeserializer.cs b/src/FeatureOne.SQL/IToggleDeserializer.cs
new file mode 100644
index 0000000..c6cc118
--- /dev/null
+++ b/src/FeatureOne.SQL/IToggleDeserializer.cs
@@ -0,0 +1,9 @@
+using FeatureOne.Core;
+
+namespace FeatureOne.SQL
+{
+ public interface IToggleDeserializer
+ {
+ IToggle Deserialize(string toggle);
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/NamePostFix.cs b/src/FeatureOne.SQL/NamePostFix.cs
similarity index 94%
rename from src/FeatureOne/Core/NamePostFix.cs
rename to src/FeatureOne.SQL/NamePostFix.cs
index 4dab788..43e500f 100644
--- a/src/FeatureOne/Core/NamePostFix.cs
+++ b/src/FeatureOne.SQL/NamePostFix.cs
@@ -1,6 +1,6 @@
using System;
-namespace FeatureOne.Core
+namespace FeatureOne.SQL
{
public class NamePostFix
{
diff --git a/src/FeatureOne.SQL/SQLConfiguration.cs b/src/FeatureOne.SQL/SQLConfiguration.cs
new file mode 100644
index 0000000..ea12d3e
--- /dev/null
+++ b/src/FeatureOne.SQL/SQLConfiguration.cs
@@ -0,0 +1,127 @@
+namespace FeatureOne.SQL
+{
+ public class SQLConfiguration
+ {
+ public SQLConfiguration()
+ {
+ FeatureTable = new FeatureTable();
+ CacheSettings = new CacheSettings();
+ ConnectionSettings = new ConnectionSettings();
+ }
+
+ ///
+ /// Database settings - Connection string and provider name.
+ ///
+ public ConnectionSettings ConnectionSettings { get; set; }
+
+ ///
+ /// Feature table name and column aliases
+ ///
+ public FeatureTable FeatureTable { get; set; }
+
+ ///
+ /// Feature cache settings.
+ ///
+ public CacheSettings CacheSettings { get; set; }
+ }
+
+ public class ConnectionSettings
+ {
+ ///
+ /// Connection string to feature database.
+ ///
+ public string ConnectionString { get; set; }
+
+ ///
+ /// Provider name for connection factory. Please see DbProviderName class for supported constants.
+ ///
+ ///
+ /// Supported providers:
+ /// System.Data.SqlClient
+ /// System.Data.Odbc
+ /// System.Data.OleDb
+ /// System.Data.SQLite
+ /// MySql.Data.MySqlClient
+ /// Npgsql
+ ///
+ public string ProviderName { get; set; } = DbProviderName.MSSql;
+ }
+
+ ///
+ /// Provider name for connection factories.
+ ///
+ public class DbProviderName
+ {
+ ///
+ /// Provider - System.Data.SqlClient
+ ///
+ public const string MSSql = "System.Data.SqlClient";
+
+ ///
+ /// Provider - System.Data.Odbc
+ ///
+ public const string Odbc = "System.Data.Odbc";
+
+ ///
+ /// Provider - System.Data.OleDb
+ ///
+ public const string OleDb = "System.Data.OleDb";
+
+ ///
+ /// Provider - System.Data.SqlClient
+ ///
+ public const string SQLite = "System.Data.SQLite";
+
+ ///
+ /// Provider - MySql.Data.MySqlClient
+ ///
+ public const string MySql = "MySql.Data.MySqlClient";
+
+ ///
+ /// Provider - Npgsql
+ ///
+ public const string PostgresSql = "Npgsql";
+ }
+
+ ///
+ /// Feature table settings to override table aliases.
+ ///
+ public class FeatureTable
+ {
+ ///
+ /// Table Name for the feature table.
+ ///
+ public string TableName { get; set; } = "TFeatures";
+
+ ///
+ /// Feature name column name.
+ ///
+ public string NameColumn { get; set; } = "Name";
+
+ ///
+ /// Feature toggle column name.
+ ///
+ public string ToggleColumn { get; set; } = "Toggle";
+
+ ///
+ /// Feature archived column name.
+ ///
+ public string ArchivedColumn { get; set; } = "Archived";
+ }
+
+ ///
+ /// Cache settings for storage provider.
+ ///
+ public class CacheSettings
+ {
+ ///
+ /// Use cache when true, default is false for no cache.
+ ///
+ public bool EnableCache { get; set; } = false;
+
+ ///
+ /// Cache item expiry value in minutes.
+ ///
+ public int ExpiryInMinutes { get; set; } = 60;
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/StorageProvider/DbRepository.cs b/src/FeatureOne.SQL/StorageProvider/DbRepository.cs
new file mode 100644
index 0000000..70c4e0b
--- /dev/null
+++ b/src/FeatureOne.SQL/StorageProvider/DbRepository.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Data.Odbc;
+using System.Data.OleDb;
+using System.Data.SqlClient;
+using System.Data.SQLite;
+using System.Linq;
+using MySql.Data.MySqlClient;
+using Npgsql;
+
+namespace FeatureOne.SQL.StorageProvider
+{
+ internal class DbRepository : IDbRepository
+ {
+ private readonly SQLConfiguration sqlConfiguration;
+ private readonly IFeatureLogger logger;
+
+ public DbRepository(SQLConfiguration sqlConfiguration, IFeatureLogger logger)
+ {
+ this.sqlConfiguration = sqlConfiguration ?? throw new ArgumentNullException(nameof(SQLConfiguration));
+ this.logger = logger;
+
+ var names = DbProviderFactories.GetProviderInvariantNames();
+
+ if (!names.Any(x => x.Equals("System.Data.SqlClient") && SqlClientFactory.Instance != null))
+ DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance);
+
+ if (!names.Any(x => x.Equals("System.Data.Odbc")) && OdbcFactory.Instance != null)
+ DbProviderFactories.RegisterFactory("System.Data.Odbc", OdbcFactory.Instance);
+
+ if (!names.Any(x => x.Equals("System.Data.OleDb")) && OleDbFactory.Instance != null)
+ DbProviderFactories.RegisterFactory("System.Data.OleDb", OleDbFactory.Instance);
+
+ if (!names.Any(x => x.Equals("System.Data.SQLite")) && SQLiteFactory.Instance != null)
+ DbProviderFactories.RegisterFactory("System.Data.SQLite", SQLiteFactory.Instance);
+
+ if (!names.Any(x => x.Equals("MySql.Data.MySqlClient")) && MySqlClientFactory.Instance != null)
+ DbProviderFactories.RegisterFactory("MySql.Data.MySqlClient", MySqlClientFactory.Instance);
+
+ if (!names.Any(x => x.Equals("Npgsql")) && NpgsqlFactory.Instance != null)
+ DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance);
+ }
+
+ public DbRecord[] GetByName(string name)
+ {
+ var dbRecords = new List();
+ try
+ {
+ var factory = DbProviderFactories.GetFactory(sqlConfiguration.ConnectionSettings.ProviderName);
+
+ if (factory == null)
+ throw new InvalidOperationException($"Provider: {sqlConfiguration.ConnectionSettings.ProviderName} is not supported. Please register entry in DbProviderFactories ");
+
+ using (var connection = factory.CreateConnection())
+ {
+ connection.ConnectionString = sqlConfiguration.ConnectionSettings.ConnectionString;
+
+ connection.Open();
+ try
+ {
+ using (var command = connection.CreateCommand())
+ {
+ command.CommandType = CommandType.Text;
+ command.CommandText = sqlConfiguration.FeatureTable.CreateSQL(name);
+
+ var reader = command.ExecuteReader();
+ while (reader.Read())
+ dbRecords.Add(new DbRecord { Name = reader.GetString(0), Toggle = reader.GetString(1) });
+
+ reader.Close();
+ }
+ }
+ catch (Exception e)
+ {
+ logger?.Error($"FeatureOne.SQL, Action='Repository.GetByName()', Exception='{e}'.");
+ }
+ finally
+ {
+ connection.Close();
+ }
+ }
+
+ logger?.Error($"FeatureOne.SQL, Action='Repository.GetByName()', Success=Feature Count'{dbRecords.Count}'.");
+
+ return dbRecords.ToArray();
+ }
+ catch (Exception ex)
+ {
+ logger?.Error($"FeatureOne.SQL, Action='Repository.GetByName()', Exception='{ex}'.");
+ }
+
+ return Array.Empty();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/StorageProvider/SQLStatement.cs b/src/FeatureOne.SQL/StorageProvider/SQLStatement.cs
new file mode 100644
index 0000000..2b319fc
--- /dev/null
+++ b/src/FeatureOne.SQL/StorageProvider/SQLStatement.cs
@@ -0,0 +1,10 @@
+namespace FeatureOne.SQL.StorageProvider
+{
+ public static class SQLStatement
+ {
+ public static string CreateSQL(this FeatureTable table, string featureName)
+ {
+ return $"Select {table.NameColumn}, {table.ToggleColumn} From {table.TableName} Where {table.ArchivedColumn} = 0 and {table.NameColumn} = '{featureName}'";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/StorageProvider/SQLStorageProvider.cs b/src/FeatureOne.SQL/StorageProvider/SQLStorageProvider.cs
new file mode 100644
index 0000000..9cc4493
--- /dev/null
+++ b/src/FeatureOne.SQL/StorageProvider/SQLStorageProvider.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using FeatureOne.Core;
+using FeatureOne.Core.Stores;
+
+namespace FeatureOne.SQL.StorageProvider
+{
+ public class SQLStorageProvider : IStorageProvider
+ {
+ internal IDbRepository repository;
+ internal IToggleDeserializer deserializer;
+ internal ICache cache;
+
+ internal CacheSettings cacheSettings;
+
+ public SQLStorageProvider(SQLConfiguration sqlConfiguration, IFeatureLogger logger = null, ICache cache = null, IConditionDeserializer conditionDeserializer = null)
+ {
+ this.cacheSettings = sqlConfiguration?.CacheSettings ?? new CacheSettings();
+ this.repository = new DbRepository(sqlConfiguration, logger ?? new NullLogger());
+ this.deserializer = new ToggleDeserializer(conditionDeserializer ?? new ConditionDeserializer());
+ this.cache = cache ?? new FeatureCache();
+ }
+
+ public SQLStorageProvider(IDbRepository repository, IToggleDeserializer deserializer, ICache cache, CacheSettings cacheSettings)
+ {
+ this.repository = repository;
+ this.deserializer = deserializer;
+ this.cache = cache;
+ this.cacheSettings = cacheSettings ?? new CacheSettings();
+ }
+
+ public IFeature[] GetByName(string name)
+ {
+ DbRecord[] dbFeatures = null;
+
+ if (cacheSettings.EnableCache)
+ dbFeatures = (DbRecord[])cache?.Get(name);
+
+ if (dbFeatures == null)
+ dbFeatures = repository.GetByName(name);
+
+ if (cacheSettings.EnableCache)
+ cache.Add(name, dbFeatures, cacheSettings.ExpiryInMinutes);
+
+ return dbFeatures != null && dbFeatures.Any()
+ ? dbFeatures.Where(x => !string.IsNullOrEmpty(x.Toggle)).Select(f => new Feature(f.Name, deserializer.Deserialize(f.Toggle))).ToArray()
+ : Array.Empty();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne.SQL/ToggleDeserializer.cs b/src/FeatureOne.SQL/ToggleDeserializer.cs
new file mode 100644
index 0000000..0d156cd
--- /dev/null
+++ b/src/FeatureOne.SQL/ToggleDeserializer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using FeatureOne.Core;
+
+[assembly: InternalsVisibleTo("FeatureOne.SQL.Tests")]
+
+namespace FeatureOne.SQL
+{
+ internal class ToggleDeserializer : IToggleDeserializer
+ {
+ private readonly IConditionDeserializer conditionDeserializer;
+
+ public ToggleDeserializer() : this(new ConditionDeserializer())
+ {
+ }
+
+ public ToggleDeserializer(IConditionDeserializer conditionDeserializer) =>
+ this.conditionDeserializer = conditionDeserializer;
+
+ public IToggle Deserialize(string toggle)
+ {
+ var jObject = JsonNode.Parse(toggle);
+
+ var toggleOperator = jObject["operator"]?.ToString() ?? Operator.Any.ToString();
+ var toggleConditions = jObject["conditions"].Deserialize();
+
+ return new Toggle
+ (
+ Enum.TryParse(toggleOperator, true, out var @operator) ? @operator : Operator.Any,
+ toggleConditions.Select(t => conditionDeserializer.Deserialize(t)).ToArray()
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne/AssemblyInfo.cs b/src/FeatureOne/AssemblyInfo.cs
index b1cd85e..7be6fd1 100644
--- a/src/FeatureOne/AssemblyInfo.cs
+++ b/src/FeatureOne/AssemblyInfo.cs
@@ -15,11 +15,11 @@
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("2023")]
[assembly: System.Reflection.AssemblyDescriptionAttribute(".Net Library to implement feature toggles.")]
-[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.7.0")]
+[assembly: System.Reflection.AssemblyFileVersionAttribute("2.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.7")]
[assembly: System.Reflection.AssemblyProductAttribute("FeatureOne")]
[assembly: System.Reflection.AssemblyTitleAttribute("FeatureOne")]
-[assembly: System.Reflection.AssemblyVersionAttribute("1.0.7.0")]
+[assembly: System.Reflection.AssemblyVersionAttribute("2.0.0.0")]
[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://github.com/NinjaRocks/FeatureOne")]
// Generated by the MSBuild WriteCodeFragment class.
diff --git a/src/FeatureOne/ClassDiagram.cd b/src/FeatureOne/ClassDiagram.cd
index b17c4b9..f44c6c1 100644
--- a/src/FeatureOne/ClassDiagram.cd
+++ b/src/FeatureOne/ClassDiagram.cd
@@ -9,6 +9,7 @@
+
@@ -24,7 +25,7 @@
AAAAIAAAAAAAAAAAAAAAAgAAAAAAAAAQAAAAAAAAAAA=
Core\Toggles\Conditions\RegexCondition.cs
-
+
@@ -38,68 +39,31 @@
-
+
- AgAAAAAAAAAAAQAAAAGAAAAAAAAAAAAAgAAAAAAAAAA=
+ AgAAAAAAAAAAAQACAAGAAAAAAAAAAAAAAAAAAAAAAAA=
Features.cs
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
- EAAAAAAAAAAAAQAAAAAAAAAAAAAgAAAAggAAAAAAAAA=
+ AAAAAAAAAAAAAAACAAAAAAAAAAAgAAAAAAAAAAACAAA=
Core\Stores\FeatureStore.cs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- AAAAAAAAAAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAA=
- Configuration.cs
-
-
-
+
-
-
-
-
- AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=
- Core\Stores\ToggleDeserializer.cs
-
+
@@ -123,32 +87,25 @@
-
+
- AAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAgAAAAAAAAA=
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAA=
IFeatureStore.cs
-
-
-
- AAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
- Core\Stores\IStoreProvider.cs
-
-
-
-
-
- AAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA=
- Core\Stores\IToggleDeserializer.cs
-
-
-
+
AAAAAAAAAAAAAYAAAAAAAAAAAAAAAAEAAAAAAEAAAAA=
IFeatureLogger.cs
+
+
+
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAg=
+ Core\Stores\IStorageProvider.cs
+
+
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/FeatureStore.cs b/src/FeatureOne/Core/Stores/FeatureStore.cs
index 061d08c..3f9d63d 100644
--- a/src/FeatureOne/Core/Stores/FeatureStore.cs
+++ b/src/FeatureOne/Core/Stores/FeatureStore.cs
@@ -1,56 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Xml.Linq;
namespace FeatureOne.Core.Stores
{
public class FeatureStore : IFeatureStore
{
- private IStorageProvider storageProvider;
- private IFeatureLogger logger;
- private IToggleDeserializer toggleDeserializer;
+ private readonly IStorageProvider storageProvider;
+ private readonly IFeatureLogger logger;
- public FeatureStore(IStorageProvider storageProvider) : this(storageProvider, new NullLogger(), new ToggleDeserializer())
+ public FeatureStore(IStorageProvider storageProvider) : this(storageProvider, new NullLogger())
{
}
- public FeatureStore(IStorageProvider storageProvider, IFeatureLogger logger) : this(storageProvider, logger, new ToggleDeserializer())
- {
- }
-
- public FeatureStore(IStorageProvider storageProvider, IFeatureLogger logger, IToggleDeserializer toggleDeserializer)
+ public FeatureStore(IStorageProvider storageProvider, IFeatureLogger logger)
{
this.storageProvider = storageProvider;
this.logger = logger;
- this.toggleDeserializer = toggleDeserializer;
- }
-
- public IEnumerable FindStartsWith(string key)
- {
- return GetAll().Where(x => x.Name.Value.StartsWith(key, StringComparison.OrdinalIgnoreCase));
}
- public IEnumerable GetAll()
+ public IEnumerable FindStartsWith(string name)
{
- var features = storageProvider.Get();
+ var features = storageProvider.GetByName(name);
if (features == null || !features.Any())
+ {
+ logger?.Info($"FeatureOne, Action='StorageProvider.Get', Message='Retrieved Features list was empty.'");
return Enumerable.Empty();
+ }
var result = new List();
- foreach (var feature in features)
- {
- try
- {
- result.Add(new Feature(feature.Key, toggleDeserializer.Deserializer(feature.Value)));
- logger?.Info($"FeatureOne, Action='StorageProvider.Get', Feature='{feature.Key}', Message='Reterieved Success'");
- }
- catch (Exception ex)
- {
- logger?.Error($"FeatureOne, Action='StorageProvider.Get', Feature='{feature.Key}', Toggle='{feature.Value}' Exception='{ex}'.");
- }
- }
+ foreach (var feature in features.Where(x => x.Toggle?.Conditions != null && x.Toggle.Conditions.Any()))
+ result.Add(feature);
return result;
}
diff --git a/src/FeatureOne/Core/Stores/IStorageProvider.cs b/src/FeatureOne/Core/Stores/IStorageProvider.cs
index 10da015..7ef239b 100644
--- a/src/FeatureOne/Core/Stores/IStorageProvider.cs
+++ b/src/FeatureOne/Core/Stores/IStorageProvider.cs
@@ -1,5 +1,3 @@
-using System.Collections.Generic;
-
namespace FeatureOne.Core.Stores
{
///
@@ -8,14 +6,9 @@ namespace FeatureOne.Core.Stores
public interface IStorageProvider
{
///
- /// Implement this method to get all feature toggless from storage.
+ /// Implement to get storage feature toggles by a given name.
///
- ///
- /// Example:
- /// Key : "Feature-01"
- /// Value : "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"
- ///
- ///
- IEnumerable> Get();
+ /// Array of Features
+ IFeature[] GetByName(string name);
}
}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/IToggleDeserializer.cs b/src/FeatureOne/Core/Stores/IToggleDeserializer.cs
deleted file mode 100644
index 71eb766..0000000
--- a/src/FeatureOne/Core/Stores/IToggleDeserializer.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace FeatureOne.Core.Stores
-{
- public interface IToggleDeserializer
- {
- IToggle Deserializer(string input);
- }
-}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/NullStoreProvder.cs b/src/FeatureOne/Core/Stores/NullStoreProvder.cs
deleted file mode 100644
index 560d088..0000000
--- a/src/FeatureOne/Core/Stores/NullStoreProvder.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace FeatureOne.Core.Stores
-{
- public class NullStoreProvder : IStorageProvider
- {
- public IEnumerable> Get()
- {
- return Enumerable.Empty>();
- }
- }
-}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/NullStoreProvider.cs b/src/FeatureOne/Core/Stores/NullStoreProvider.cs
new file mode 100644
index 0000000..fdea649
--- /dev/null
+++ b/src/FeatureOne/Core/Stores/NullStoreProvider.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace FeatureOne.Core.Stores
+{
+ public class NullStoreProvider : IStorageProvider
+ {
+ public IFeature[] GetByName(string name) => Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Stores/ToggleDeserializer.cs b/src/FeatureOne/Core/Stores/ToggleDeserializer.cs
deleted file mode 100644
index 57a946c..0000000
--- a/src/FeatureOne/Core/Stores/ToggleDeserializer.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-using System.Text.Json;
-using System.Text.Json.Nodes;
-
-namespace FeatureOne.Core.Stores
-{
- public class ToggleDeserializer : IToggleDeserializer
- {
- public IToggle Deserializer(string input)
- {
- var jObject = JsonNode.Parse(input);
-
- var toggleOperator = jObject["operator"]?.ToString() ?? Operator.Any.ToString();
- var toggleConditions = jObject["conditions"].Deserialize();
-
- return new Toggle
- (
- Enum.TryParse(toggleOperator, true, out var @operator) ? @operator : Operator.Any,
- ConditionFactory.Create(toggleConditions)
- );
- }
- }
-}
\ No newline at end of file
diff --git a/src/FeatureOne/Core/Toggle.cs b/src/FeatureOne/Core/Toggle.cs
index b7a0a4d..4ac4b4e 100644
--- a/src/FeatureOne/Core/Toggle.cs
+++ b/src/FeatureOne/Core/Toggle.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -5,10 +6,14 @@ namespace FeatureOne.Core
{
public class Toggle : IToggle
{
- public Toggle(Operator @operator, ICondition[] conditions)
+ public Toggle() : this(Operator.Any, Array.Empty())
+ {
+ }
+
+ public Toggle(Operator @operator, params ICondition[] conditions)
{
Operator = @operator;
- Conditions = conditions;
+ Conditions = conditions ?? Array.Empty();
}
public Operator Operator { get; set; }
diff --git a/src/FeatureOne/FeatureName.cs b/src/FeatureOne/FeatureName.cs
index f7ff851..f4559b0 100644
--- a/src/FeatureOne/FeatureName.cs
+++ b/src/FeatureOne/FeatureName.cs
@@ -18,5 +18,7 @@ public FeatureName(string name)
public string Value { get; }
public static implicit operator string(FeatureName s) => s?.Value;
+
+ public static implicit operator FeatureName(string s) => new FeatureName(s);
}
}
\ No newline at end of file
diff --git a/src/FeatureOne/FeatureOne.csproj b/src/FeatureOne/FeatureOne.csproj
index 7a64cf9..e820148 100644
--- a/src/FeatureOne/FeatureOne.csproj
+++ b/src/FeatureOne/FeatureOne.csproj
@@ -18,21 +18,23 @@
Ninja.Sha!4H
FeatureOne
.Net library to implement feature toggles.
- 2023
+ Copyright (c) 2023 Ninja Sha!4h
README.md
https://github.com/NinjaRocks/FeatureOne
git
feature-toggle; feature-flag; feature-flags; feature-toggles; .netstandard2.1; featureOne
- 1.0.7
+ 2.0.0
LICENSE.md
ninja-icon-16.png
- Release Notes v1.0.7 - release to target .net standard 2.1.
-
-Core Functionality :-
-- To Implement Feature Toggles to hide/show program features.
-- Provides Out of box Simple and Regex toggle conditions that can be extended for any use case.
-- Provides Extensibility for custom implementation of backend storages.
-
+
+ Release Notes v2.0.0. - Breaking changes to previous release.
+ Core Functionality :-
+ - Contract changes to IStorageProvider - Get() method returns array of IFeature type.
+ - To Implement Feature Toggles to hide/show program features.
+ - Provides Out of box Simple and Regex toggle conditions .
+ - Provides extensibility to implement custom toggle conditions for any bespoke use case.
+ - Provides extensibility to implement backend storage. No Backend storage exists by default.
+
@@ -50,8 +52,5 @@ Core Functionality :-
-
-
-
+
-
diff --git a/src/FeatureOne/Features.cs b/src/FeatureOne/Features.cs
index 06cbd20..4a54ad6 100644
--- a/src/FeatureOne/Features.cs
+++ b/src/FeatureOne/Features.cs
@@ -25,6 +25,16 @@ public Features(IFeatureStore featureStore, IFeatureLogger logger)
public static void Initialize(Func factory) => Current = factory();
+ ///
+ /// Determines whether the feature is enabled.
+ ///
+ /// feature name
+ ///
+ public bool IsEnabled(string name)
+ {
+ return IsEnabled(name, Enumerable.Empty());
+ }
+
///
/// Determines whether the feature is enabled for given claims principal.
///
diff --git a/src/FeatureOne/IFeatureStore.cs b/src/FeatureOne/IFeatureStore.cs
index cfffd39..3c7c576 100644
--- a/src/FeatureOne/IFeatureStore.cs
+++ b/src/FeatureOne/IFeatureStore.cs
@@ -5,7 +5,5 @@ namespace FeatureOne
public interface IFeatureStore
{
IEnumerable FindStartsWith(string key);
-
- IEnumerable GetAll();
}
}
\ No newline at end of file
diff --git a/test/FeatureOne.SQL.Tests/App.config b/test/FeatureOne.SQL.Tests/App.config
new file mode 100644
index 0000000..48dc347
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/App.config
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/FeatureOne.SQL.Tests/E2e Tests/E2eLogger.cs b/test/FeatureOne.SQL.Tests/E2e Tests/E2eLogger.cs
new file mode 100644
index 0000000..dc7d60d
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/E2e Tests/E2eLogger.cs
@@ -0,0 +1,10 @@
+namespace FeatureOne.SQL.Tests.E2e
+{
+ internal class E2eLogger : IFeatureLogger
+ {
+ public void Debug(string message) => Console.WriteLine("Debug:" + message);
+ public void Error(string message) => Console.WriteLine("Error:" + message);
+ public void Info(string message) => Console.WriteLine("Info:" + message);
+ public void Warn(string message) => Console.WriteLine("Warn:" + message);
+ }
+}
\ No newline at end of file
diff --git a/test/FeatureOne.SQL.Tests/E2e Tests/End2EndTests.SQLite.cs b/test/FeatureOne.SQL.Tests/E2e Tests/End2EndTests.SQLite.cs
new file mode 100644
index 0000000..67a2c9e
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/E2e Tests/End2EndTests.SQLite.cs
@@ -0,0 +1,48 @@
+using System.Security.Claims;
+using FeatureOne.Core.Stores;
+using FeatureOne.SQL.StorageProvider;
+using NUnit.Framework.Internal;
+
+namespace FeatureOne.SQL.Tests.E2e
+{
+ public class End2EndTests
+ {
+ [SetUp]
+ public void Setup()
+ {
+ var connectionString = $"DataSource={Environment.CurrentDirectory}//Features.db;mode=readonly;cache=shared";
+
+ Console.WriteLine(connectionString);
+
+ var configuration = new SQLConfiguration { ConnectionSettings = new ConnectionSettings { ConnectionString = connectionString, ProviderName = DbProviderName.SQLite } };
+
+ var logger = new E2eLogger();
+
+ var provider = new SQLStorageProvider(configuration);
+
+ Features.Initialize(() => new Features(new FeatureStore(provider, logger), logger));
+ }
+
+ [Test]
+ public void TestForDashboardWidgetToBeEnabled()
+ {
+ var enabled = Features.Current.IsEnabled("dashboard_widget");
+ Assert.IsTrue(enabled);
+ }
+
+ [Test]
+ public void TestForGBKDashboardToBeEnabledForUsersWithGBKEmails()
+ {
+ var enabled = Features.Current.IsEnabled("gbk_dashboard");
+ Assert.False(enabled);
+
+ var user1_claims = new[] { new Claim("email", "ninja@udt.com") };
+ enabled = Features.Current.IsEnabled("gbk_dashboard", user1_claims);
+ Assert.False(enabled);
+
+ var user2_claims = new[] { new Claim("email", "ninja@gbk.com") };
+ enabled = Features.Current.IsEnabled("gbk_dashboard", user2_claims);
+ Assert.True(enabled);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/FeatureOne.SQL.Tests/FeatureOne.SQL.Tests.csproj b/test/FeatureOne.SQL.Tests/FeatureOne.SQL.Tests.csproj
new file mode 100644
index 0000000..520e101
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/FeatureOne.SQL.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/test/FeatureOne.SQL.Tests/Features.db b/test/FeatureOne.SQL.Tests/Features.db
new file mode 100644
index 0000000..bc781e3
Binary files /dev/null and b/test/FeatureOne.SQL.Tests/Features.db differ
diff --git a/test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs b/test/FeatureOne.SQL.Tests/UnitTests/ConditionDeserializerTest.cs
similarity index 62%
rename from test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs
rename to test/FeatureOne.SQL.Tests/UnitTests/ConditionDeserializerTest.cs
index e872c45..4aedce6 100644
--- a/test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs
+++ b/test/FeatureOne.SQL.Tests/UnitTests/ConditionDeserializerTest.cs
@@ -1,17 +1,17 @@
-using FeatureOne.Core.Stores;
using FeatureOne.Core.Toggles.Conditions;
+using FeatureOne.SQL;
using System.Text.Json.Nodes;
-namespace FeatureOne.Test.Toggles
+namespace FeatureOne.SQL.Tests.UnitTests
{
[TestFixture]
- public sealed class ToggleConditionFactoryTest
+ public sealed class ConditionDeserializerTest
{
[Test]
public void TestToggleConditionForNUllInput()
{
JsonObject jObj = null;
- Assert.Throws(() => ConditionFactory.Create(jObj));
+ Assert.Throws(() => new ConditionDeserializer().Deserialize(jObj));
}
[Test]
@@ -19,8 +19,8 @@ public void TestToggleConditionForCorrectSimpleInstanceType()
{
var json = "{\r\n\t\t\t \"type\":\"Simple\",\r\n\t\t\t \"IsEnabled\":\"true\"\r\n\t\t}";
- var jobject = JsonObject.Parse(json)?.AsObject();
- var toggleCondition = ConditionFactory.Create(jobject);
+ var jobject = JsonNode.Parse(json)?.AsObject();
+ var toggleCondition = new ConditionDeserializer().Deserialize(jobject);
Assert.IsInstanceOf(typeof(SimpleCondition), toggleCondition);
}
@@ -30,8 +30,8 @@ public void TestToggleConditionForCorrectRegexInstanceType()
{
var json = "{\r\n\t\t\t \"type\":\"RegexCondition\",\r\n\t\t\t \"claim\":\"email\",\r\n\t\t\t \"expression\":\"*@gbk.com\"\r\n\t\t }";
- var jobject = JsonObject.Parse(json)?.AsObject();
- var toggleCondition = ConditionFactory.Create(jobject);
+ var jobject = JsonNode.Parse(json)?.AsObject();
+ var toggleCondition = new ConditionDeserializer().Deserialize(jobject);
Assert.IsInstanceOf(typeof(RegexCondition), toggleCondition);
}
diff --git a/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs b/test/FeatureOne.SQL.Tests/UnitTests/NamePostFixTest.cs
similarity index 91%
rename from test/FeatureOne.Tests/Toggles/NamePostFixTest.cs
rename to test/FeatureOne.SQL.Tests/UnitTests/NamePostFixTest.cs
index e38f6dd..37ae4ec 100644
--- a/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs
+++ b/test/FeatureOne.SQL.Tests/UnitTests/NamePostFixTest.cs
@@ -1,6 +1,4 @@
-using FeatureOne.Core;
-
-namespace FeatureOne.Tests.Toggles
+namespace FeatureOne.SQL.Tests.UnitTests
{
[TestFixture]
public class NamePostFixTest
diff --git a/test/FeatureOne.SQL.Tests/UnitTests/SQLStatementTest.cs b/test/FeatureOne.SQL.Tests/UnitTests/SQLStatementTest.cs
new file mode 100644
index 0000000..8d03e83
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/UnitTests/SQLStatementTest.cs
@@ -0,0 +1,16 @@
+using FeatureOne.SQL.StorageProvider;
+
+namespace FeatureOne.SQL.Tests.UnitTests
+{
+ [TestFixture]
+ internal class SQLStatementTest
+ {
+ [Test]
+ public void TestCreateSQL()
+ {
+ var table = new FeatureTable();
+ var sql = table.CreateSQL("Foo");
+ Assert.AreEqual("Select Name, Toggle From TFeatures Where Archived = 0 and Name = 'Foo'", sql);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/FeatureOne.SQL.Tests/UnitTests/SQLStorageProviderTest.cs b/test/FeatureOne.SQL.Tests/UnitTests/SQLStorageProviderTest.cs
new file mode 100644
index 0000000..5029f18
--- /dev/null
+++ b/test/FeatureOne.SQL.Tests/UnitTests/SQLStorageProviderTest.cs
@@ -0,0 +1,95 @@
+using System.Runtime.Caching;
+using FeatureOne.SQL.StorageProvider;
+using Moq;
+
+namespace FeatureOne.SQL.Tests.UnitTests
+{
+ internal class SQLStorageProviderTest
+ {
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void TestStorageProviderInit()
+ {
+ var cacheSettings = new CacheSettings();
+ var configuration = new SQLConfiguration { CacheSettings = cacheSettings };
+
+ var provider = new SQLStorageProvider(configuration);
+
+ Assert.IsNotNull(provider.cacheSettings);
+ Assert.That(provider.cacheSettings, Is.EqualTo(cacheSettings));
+ Assert.IsNotNull(provider.repository);
+ Assert.IsNotNull(provider.deserializer);
+ Assert.IsNotNull(provider.cache);
+
+ provider = new SQLStorageProvider(configuration, new Mock().Object);
+
+ Assert.IsNotNull(provider.cacheSettings);
+ Assert.That(provider.cacheSettings, Is.EqualTo(cacheSettings));
+ Assert.IsNotNull(provider.repository);
+ Assert.IsNotNull(provider.deserializer);
+ Assert.IsNotNull(provider.cache);
+
+ provider = new SQLStorageProvider(new SQLConfiguration(), new Mock().Object, new Mock().Object, new Mock().Object);
+
+ Assert.IsNotNull(provider.cacheSettings);
+ Assert.That(provider.cacheSettings, Is.Not.EqualTo(cacheSettings));
+ Assert.IsNotNull(provider.repository);
+ Assert.IsNotNull(provider.deserializer);
+ Assert.IsNotNull(provider.cache);
+
+ provider = new SQLStorageProvider(new Mock().Object, new Mock().Object, new Mock().Object, null);
+
+ Assert.IsNotNull(provider.cacheSettings);
+ Assert.That(provider.cacheSettings, Is.Not.EqualTo(cacheSettings));
+ Assert.IsNotNull(provider.repository);
+ Assert.IsNotNull(provider.deserializer);
+ Assert.IsNotNull(provider.cache);
+ }
+
+ [Test]
+ public void TestGetMethodToUseNoCache()
+ {
+ var cache = new Mock();
+
+ var repository = new Mock();
+ repository.Setup(x => x.GetByName("Foo")).Returns(new[] { new DbRecord { Name = "Foo", Toggle = "Toggle" } });
+
+ var deserializer = new Mock();
+ var provider = new SQLStorageProvider(repository.Object, deserializer.Object, cache.Object, new CacheSettings());
+
+ var result = provider.GetByName("Foo");
+
+ repository.Verify(x => x.GetByName("Foo"), Times.Once());
+ deserializer.Verify(x => x.Deserialize(It.IsAny()), Times.Once());
+
+ cache.Verify(x => x.Get(It.IsAny()), Times.Never());
+ cache.Verify(x => x.Add(It.IsAny(), It.IsAny