diff --git a/DeveloperGuide.md b/DeveloperGuide.md new file mode 100644 index 0000000..afab0e1 --- /dev/null +++ b/DeveloperGuide.md @@ -0,0 +1,490 @@ +# Developer Guide + +## i. Installation +Install the latest nuget package as appropriate. + +`FeatureOne` - for installing FeatureOne for custom `IStorageProvider` implementation. +``` +NuGet\Install-Package FeatureOne +``` +`FeatureOne.SQL` - for installing FeatureOne with SQL storage provider. +``` +NuGet\Install-Package FeatureOne.SQL +``` +`FeatureOne.File` - for installing FeatureOne with File system storage provider. +``` +NuGet\Install-Package FeatureOne.File +``` + +ii. Implementation: How to use FeatureOne +-- +### Step 1. Add Feature IsEnabled Check in Code. +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 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.Current.IsEnable(featureName){ // See other IsEnable() overloads + showDashboardWidget(); +} +``` + + +### Step 2. Add Feature Toggle Definition to Storage +Add a `toggle` definition to storage ie. a store in database or file or other storage medium. +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). + +Below is a serialized JSON representation of a Feature Toggle. +``` +{ + "feature_name":{ -- Feature name + "toggle":{ -- Toggle definition for the feature + + "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 + + .... other type specific properties, See below for details. + }] + } + } +} +``` + +### Condition Types +There are two types of toggle conditions that can be used out of box. + +#### i. Simple Condition +`Simple` condition allows toggle with simple enable or disable of the given feature. User claims are not taken into account for this condition. + +Below is the serialized representation of toggle with simple condition. +``` +{ + "dashboard_widget":{ + "toggle":{ + "conditions":[{ + "type":"Simple", -- Simple Condition. + "isEnabled":true|false -- Enabled or disable the feature. + }] + } + } +} +``` +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 value to enable a given feature. + +Below is the serialized representation of toggle with regex condition. +``` + { + "dashboard_widget":{ + "toggle":{ + + "conditions":[{ + "type":"Regex", -- Regex Condition + "claim":"email", -- Claim 'email' to be used for evaluation. + "expression":"*@gbk.com" -- Regex expression to be used for evaluation. + }] + } + } + } +``` +C# representation of a feature with regex toggle is +``` + +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 to get storage feature toggles by a given name. + /// + /// 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. +``` +public class CustomStoreProvider : IStorageProvider + { + public Feature[] GetByName(string name) + { + return new[] { + 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" })) + }; + } + } + +``` +### Step 4. Bootstrap Initialialization +In bootstrap code, initialize the `Features` class with dependencies as shown below. + +i. With `storage provider` implementation. +``` + 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 CustomStorageProviderImpl(); + + Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger)); +``` + +How to Extend FeatureOne +-- + +### i. Toggle Condition +You could implement your own condition by extending the `ICondition` interface. +The interface provides `evaluate()` method that returns a boolean 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 empty + /// + bool Evaluate(IDictionary claims); + } +``` +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; + + 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 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 allow access after 12 o'clock. + "Hour":14 + }, + { + "type":"Regex", -- Regex to allow admin access + "claim":"role", + "expression":"^administrator$" + }] + } + } + +``` + +`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. + +### 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. +>This implementation is optional and when no logger is provided FeatureOne will not log any errors, warnings or information. +``` + /// + /// 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 + /// exception + void Error(string message, Exception ex = null); + + /// + /// Implement the info log method + /// + /// log message + void Info(string message); + + /// + /// Implement the warn log method + /// + /// log message + void Warn(string message); + } +``` +## FeatureOne.SQL - Feature toggles with SQL Backend. +In addition to all FeatureOne 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 {latest} +``` +### Step 1 - Configure Database Provider +To register a database provider, You need to add the relevant db factory with a specific `ProviderName` to `DbProviderFactories` in the bootstrap code. +ie. +`DbProviderFactories.RegisterFactory("ProviderName", ProviderFactory)` + +After adding the provider factory you need to pass the same provider in the `connection settings` of SQLConfiguration. + +> Below is the list of most common provider factories yu could configure. +> + - MSSQL - DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance); + - ODBC - DbProviderFactories.RegisterFactory("System.Data.Odbc", OdbcFactory.Instance); + - OleDb - DbProviderFactories.RegisterFactory("System.Data.OleDb", OleDbFactory.Instance); + - SQLite - DbProviderFactories.RegisterFactory("System.Data.SQLite", SQLiteFactory.Instance); + - MySQL - DbProviderFactories.RegisterFactory("MySql.Data.MySqlClient", MySqlClientFactory.Instance); + - PostgreSQL - DbProviderFactories.RegisterFactory("Npgsql", NpgsqlFactory.Instance); +> + +### STEP 2 - Setup Feature Table (Database) +> 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| + +### STEP 3 - Bootstrap initialization +> See below bootstrap initialization for FeatureOne with MS SQL backend. + + +#### SQL Configuration - Set connection string and other settings. +``` + var sqlConfiguration = new SQLConfiguration + { + // provider specific connection settings. + ConnectionSettings = new ConnectionSettings + { + Providername = "System.Data.SqlClient", -- same provider name as register with db factory. + ConnectionString ="Data Source=Powerstation; Initial Catalog=Features; Integrated Security=SSPI;" + }, + + // Table and column name overrides. + FeatureTable = new FeatureTable + { + TableName = "[Features].[dbo].[TFeatures]", + NameColumn = "[Name]", + ToggleColumn = "[Toggle]", + ArchivedColumn = "[Archived]" + }, + + // Enable cache with absolute expiry in Minutes. + CacheSettings = new CacheSettings + { + EnableCache = true, + Expiry = new CacheExpiry + { + InMinutes = 60, + Type = CacheExpiryType.Absolute + } + } + } +``` +i. With SQL configuration. +``` + -- Register db factory + DbProviderFactories.RegisterFactory("System.Data.SqlClient", SqlClientFactory.Instance); + + 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, featureCache, toggleConditionDeserializer); + + Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger)); +``` + +## FeatureOne.File - Feature toggles with File system Backend. +In addition to all FeatureOne offerings, the `FeatureOne.File` package provides out of box File storage provider. + +File support can easily be installed as a separate nuget package. +``` +$ dotnet add package FeatureOne.File --version {latest} +``` +### File Setup +> Requires creating a feature file with JSON feature toggles as shown below. + +File - `Features.json` +``` +{ + "gbk_dashboard": { + "toggle": { + "operator": "any", + "conditions": [{ + "type": "simple", + "isEnabled": false + }, + { + "type": "Regex", + "claim": "email", + "expression": "^[a-zA-Z0-9_.+-]+@gbk.com" + } + ] + } + }, + "dashboard_widget": { + "toggle": { + "conditions": [{ + "type": "simple", + "isEnabled": true + }] + } + } +} +``` +### Bootstrap initialization +> See below bootstrap initialization for FeatureOne with SQL backend. + + +#### File Configuration - Set file path string and cache settings. +``` + var configuration = new FileConfiguration + { + // Absolute path to the feature file. + FilePath ="C:\Work\Features.json", + + // Enable cache with absolute expiry in Minutes. + CacheSettings = new CacheSettings + { + EnableCache = true, + Expiry = new CacheExpiry + { + InMinutes = 60, + Type = CacheExpiryType.Absolute + } + } + } +``` +i. With File configuration. +``` + var storageProvider = new FileStorageProvider(configuration); + Features.Initialize(() => new Features(new FeatureStore(configuration))); +``` +ii. With Custom logger implementation, default is no logger. +``` + var logger = new CustomLoggerImpl(); + var storageProvider = new FileStorageProvider(configuration, 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 FileStorageProvider(configuration, featureCache, toggleConditionDeserializer); + + Features.Initialize(() => new Features(new FeatureStore(storageProvider, logger), logger)); +``` +