I've written a library that uses generics and funcs to implement a feature toggle system without the need to use if statements.
This was mostly to hone my skills, as I'm still new to this profession. I'd really appreciate any comments on how to improve the code.
It can be found in this following git repo
An extract of the main class is below Feature Toggle Class
namespace FeatureToggle.Classes
{
using System;
using Enums;
using Interfaces;
public class FeatureToggle <T> : IFeatureToggle <T>
{
private ToggleStatus Status(bool active)
{
return active ?
ToggleStatus.Active :
ToggleStatus.Inactive;
}
public ToggleStatus GetToggleState(IConfigParser parser, string toggleKey)
{
return Status(parser.GetToggleStatus(toggleKey));
}
public void ExecuteMethodIfToggleOn(Action methodToRun, IConfigParser configParser, string keyName)
{
var response = GetToggleState(configParser, keyName);
if (response == ToggleStatus.Active)
{
methodToRun();
}
}
public void ExecuteMethodIfToggleOn(Action methodToRun, string keyName)
{
IConfigParser configParser = new ConfigParser();
ExecuteMethodIfToggleOn(methodToRun, configParser, keyName);
}
public T ExecuteMethodIfToggleOn(Func<T> methodToRun, string keyName)
{
IConfigParser configParser = new ConfigParser();
return ExecuteMethodIfToggleOn(methodToRun, configParser, keyName);
}
public T ExecuteMethodIfToggleOn(Func<T> methodToRun, IConfigParser configParser, string keyName)
{
var response = GetToggleState(configParser, keyName);
if (response == ToggleStatus.Active)
{
return methodToRun();
}
return default(T);
}
}
}
Feature Toggle Interface
namespace FeatureToggle.Interfaces
{
using System;
using Enums;
public interface IFeatureToggle <T>
{
ToggleStatus GetToggleState(IConfigParser parser, string toggleKey);
void ExecuteMethodIfToggleOn(Action methodToRun, string keyName);
T ExecuteMethodIfToggleOn(Func<T> methodToRun, string keyName);
}
}
Config Parser Class
namespace FeatureToggle.Classes
{
using System;
using System.Collections.Specialized;
using System.Configuration;
using Interfaces;
public class ConfigParser : IConfigParser
{
private readonly NameValueCollection _toggles;
public ConfigParser()
{
if (ToggleConfigTagExists())
{
_toggles = ConfigurationManager.GetSection("Toggles") as NameValueCollection;
}
}
public bool ToggleConfigTagExists()
{
var toggleSection = ConfigurationManager.GetSection("Toggles");
return toggleSection != null;
}
public bool GetToggleStatus(string toggle)
{
return ParseBoolValueFromConfig(_toggles.GetValues(toggle)?[0]);
}
public bool ParseBoolValueFromConfig(string status)
{
if (status == "1" || status.ToLower() == "true")
{
return true;
}
if (status == "0" || status.ToLower() == "false")
{
return false;
}
else
{
throw new ArgumentOutOfRangeException();
}
}
}
}
Config Parser Interface
namespace FeatureToggle.Interfaces
{
public interface IConfigParser
{
bool GetToggleStatus(string toggle);
bool ParseBoolValueFromConfig(string status);
}
}
Integration tests
namespace FeatureToggleTests.Integration
{
using System;
using FeatureToggle.Classes;
using FeatureToggle.Enums;
using FeatureToggle.Interfaces;
using NUnit.Framework;
[TestFixture]
public class FeatureToggleIntegrationTests
{
[Test]
public void TestToggleStatusActiveIsReturnedWhenParsingAnItemThatIsToggledOn()
{
IConfigParser configParser = new ConfigParser();
IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
var toggleStatus = featureToggle.GetToggleState(configParser, "ButtonToggle");
Assert.AreEqual(ToggleStatus.Active, toggleStatus);
}
[Test]
public void TestToggleStatusInactiveIsReturnedWhenParsingAnItemThatIsToggledOff()
{
IConfigParser configParser = new ConfigParser();
IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
var toggleStatus = featureToggle.GetToggleState(configParser, "NotFinished");
Assert.AreEqual(ToggleStatus.Inactive, toggleStatus);
}
[Test]
public void TestOutOfRangeExceptionIsReturnedWhenParsingAnItemThatIsToggledAsdf()
{
var configParser = new ConfigParser();
IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
Assert.Throws<ArgumentOutOfRangeException>(() => featureToggle.GetToggleState(configParser, "asdf"));
}
[Test]
public void TestNullReferenceExceptionIsReturnedWhenParsingAnItemThatDoesNotExist()
{
var configParser = new ConfigParser();
IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
Assert.Throws<NullReferenceException>(() => featureToggle.GetToggleState(configParser, "wewewewewewewewe"));
}
[Test]
public void TestFakeMethodWillNotChangeValueIfConfigItemIsToggledToFalse()
{
var changeMe = "Unchanged";
FakeMethod("FakeFalse", out changeMe);
Assert.AreEqual("Unchanged", changeMe);
}
[Test]
public void TestFakeMethodWillChangeValueIfConfigItemIsToggledToTrue()
{
var changeMe = "Unchanged";
FakeMethod("FakeTrue", out changeMe);
Assert.AreEqual("has been changed", changeMe);
}
[Test]
public void TestActionFakeMethodThatReturnsTrueWillReturnTrueIfConfigItemIsToggledToTrue()
{
IFeatureToggle<bool> featureToggler = new FeatureToggle<bool>();
var result = featureToggler.ExecuteMethodIfToggleOn(FakeMethodThatReturnsTrue, "FakeTrue");
Assert.IsTrue(result);
}
[Test]
public void TestActionFakeMethodThatReturnsTrueWillReturnFalseIfConfigItemIsToggledToFalse()
{
IFeatureToggle<bool> featureToggler = new FeatureToggle<bool>();
var result = featureToggler.ExecuteMethodIfToggleOn(FakeMethodThatReturnsTrue, "FakeFalse");
Assert.IsFalse(result);
}
protected void FakeMethod(string keyName, out string changeMe)
{
IConfigParser configParser = new ConfigParser();
IFeatureToggle<bool> featureToggle = new FeatureToggle<bool>();
var response = featureToggle.GetToggleState(configParser, keyName);
if (response == ToggleStatus.Active)
{
changeMe = "has been changed";
return;
}
changeMe = "Unchanged";
}
protected bool FakeMethodThatReturnsTrue()
{
return true;
}
}
}
Unit Tests
namespace FeatureToggleTests.Unit
{
using System;
using FeatureToggle.Classes;
using FeatureToggle.Enums;
using FeatureToggle.Interfaces;
using NUnit.Framework;
[TestFixture]
public class FeatureToggleTests
{
[Test]
public void TestSuccessfullParseReturnsToggleStatusActive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<bool>();
var toggleResponse = toggle.GetToggleState(testParser, "positive");
Assert.AreEqual(ToggleStatus.Active, toggleResponse);
}
[Test]
public void TestUnSuccessfullParseReturnsToggleStatusInactive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<bool>();
var toggleResponse = toggle.GetToggleState(testParser, "anythingElse");
Assert.AreEqual(ToggleStatus.Inactive, toggleResponse);
}
[Test]
public void TestSuccessfullFuncCallWhenToggleStatusActive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<bool>();
Func<bool> theAction = AlwaysReturnTrue;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");
Assert.AreEqual(true, toggleResponse);
}
[Test]
public void TestUnSuccessfullFuncCallWhenToggleStatusInactive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<bool>();
Func<bool> theAction = AlwaysReturnTrue;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
Assert.AreEqual(false, toggleResponse);
}
[Test]
public void TestSuccessfullFuncStringCallWhenToggleStatusActive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<string>();
Func<string> theAction = AlwaysReturnFire;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");
Assert.AreEqual("Fire", toggleResponse);
}
[Test]
public void TestUnSuccessfullFuncStringCallWhenToggleStatusInactive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<string>();
Func<string> theAction = AlwaysReturnFire;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
Assert.AreNotEqual("Fire", toggleResponse);
}
[Test]
public void TestSuccessfullFuncTestDataTypeCallWhenToggleStatusActive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<TestDataType>();
Func<TestDataType> theAction = AlwaysReturnNewTestDataType;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "positive");
Assert.AreEqual(new TestDataType().HappynessIs, toggleResponse.HappynessIs);
}
[Test]
public void TestUnSuccessfullFuncTestDataTypeCallWhenToggleStatusInactive()
{
IConfigParser testParser = new ConfigParserTestDouble();
var toggle = new FeatureToggle<TestDataType>();
Func<TestDataType> theAction = AlwaysReturnNewTestDataType;
var toggleResponse = toggle.ExecuteMethodIfToggleOn(theAction, testParser, "anythingElse");
Assert.IsNull(toggleResponse);
}
protected bool AlwaysReturnTrue()
{
return true;
}
protected string AlwaysReturnFire()
{
return "Fire";
}
protected TestDataType AlwaysReturnNewTestDataType()
{
return new TestDataType();
}
}
public class TestDataType
{
public string HappynessIs = "Happy";
}
internal class ConfigParserTestDouble : IConfigParser
{
public bool ToggleConfigTagExists()
{
throw new System.NotImplementedException();
}
public bool GetToggleStatus(string toggle)
{
return toggle == "positive";
}
public bool ParseBoolValueFromConfig(string status)
{
throw new System.NotImplementedException();
}
}
}
Enum
namespace FeatureToggle.Enums
{
public enum ToggleStatus
{
Active,
Inactive
}
}
Explanation of classes:
FeatureToggle class consumes generic types and a configParser is injected in. When you want to toggle a method in your main library you call FeatureToggle.ExecuteMethodIfToggleOn(). Since it uses generic types it will output your return type/do stuff in method if the "toggle" status is on.
ConfigParser class parses out a true or false value from app.config. though it is implementing IConfigParser so I can extend the behaviour to read from a database or text file or whatever else I might need to read toggle values from.
The enum was there to make an explicit indication if something is toggled on or off, though I think this is over kill and a bool would be fine.
The integration and unit tests are there to prove this works and will pickup any bugs I introduce in any further changes. These tests just implement some quick test doubles with simple behaviour to ensure I can assert against an expected value.
ConfiguarionManager
, that contains all the strings that correlate to some boolean value, why don't you just retrieve the value and determine whether to execute the method or not just based on that? I'm hesitant to write and an answer as I'm feeling there's something I'm missing. \$\endgroup\$