In modern (agile) (micro)service oriented application landscapes, you want to deliver features as soon as possible. Preferably, through an automated deployment pipeline. However, you want to separate releasing features from deployments, and you want to have control when and to whom to release functionality (or, when and to whom to revoke functionality). Putting features behind a flag that can be enabled and disabled for groups can help achieve this goal.
Using feature flags to drive your product’s release strategy and continuous delivery
Introduction
This article provides the concept of Feature Flags, aka Feature Toggles; after that, a possible design follows with pros and cons; then, two possible implementations follow; one in Java (11), one in C# (7).
Last but not least, a wrapup and conclusion is given with some (I trust) good advice.
In short, Feature Flagging allows to modify system behavior by configuration, without changing code. So, in your production application environment, functionality ("features") can be switched on and off for certain (groups of) users without (re-, rollback-) deployment of binaries. This concept is a pattern of the type "Separation of concern", or "Single responsibility". After all, releasing binaries (deployment) and releasing functionality are separated from each other. In general, it is a good idea not to combine two responsibilities in one action (or component). By applying feature flagging, a new release with new features can be deployed, without releasing per-se the new features to everyone. And even so, it is possible to revoke features for certain groups or individuals, without having to rollback a binary release (deployment of a previous version).
See related articles:
- Roland Roos, Aspect-Oriented Programming and More Patterns in Micro-Services
- Martin Fowler, feature-toggles
- Stack Flow What-is-a-feature-flag
- Other possible C# implementation example
How is this article built up?
- Examples in Java and C# of final usage
- Concept of Feature Flagging/Toggling
- High level design and rationale (pros and cons of the design)
- Java implementation details
- C# implementation details
- Wrap-up and conclusions, and more
Before Reading this Article
Before you start reading this article, a general advice; If you are not familiar with aspect-orientation, it is advised to first read my article on this or the links in there, as the design and implementation in this article relies heavily on aspect-orientation. See [1.] Aspect-Oriented Programming and More Patterns in Micro-Services
Using the Code in Java (11+, Spring boot 2.2+, MVC thymeleaf, Aspectj)
featureFlags:
feature1:
enabled : true
included :
user1
@FeatureFlag(features = {"feature1"}, paramNameForKeyOfFeature = "user")
public void annotatedFeatureOnController(
String user,
Model model,
ModelAndView modelandView) {
if (user.compereTo("user1") == 0) {
assert.isTrue(modelandView["feature1"]);
}
}
...
Controller.annotatedFeatureOnController("user1", viewModel);
Using the Code in C# (7>, .NET MVC Core 2.1+, Autofac, Caste Core Dynamic Proxy)
"featureFlags": [
{
"name": "feature1",
"enabled": true,
"included": [
"user1"
]
}
]
[FeatureFlag("user", new String[] { "feature1" })]
public ActionResult AnnotatedFeatureOnController([FeatureFlagKey] String user)
{
if (user.compereTo("user1") == 0)
{
assert.isTrue( ViewData["feature1"]);
}
}
...
HomeController.AnnotatedFeatureOnController("user1");
or
http:
Concept: The Basic Principles
What Is Feature Flagging?
There are already good articles on this, in general. See also [2.] Martin Fowler, feature-toggles
Remark: Feature toggle and feature flag are synonyms.
A flag in coding-land is commonly known as a boolean 1/0, true/false, on/off. So, the feature is either on or off.
A toggle in coding-land is generally the more dynamic flipping of the flag, 0<->1 , true <-> false, on <-> off. So it refers to flipping the feature from on to off, and vice-versa.
Why Use Feature Flagging?
In modern, micro-service oriented application landscapes that use a "real" agile approach, we would like to deploy and release functionality (features) as early as possible. Preferably, right after it is automatically built, and automatically tested as ok in the build pipeline. However, we don't want this feature to be always released and available to "everyone" once deployed. Wouldn't it be nice to have the functionality deployed to production at once, skipping complex acceptation periods? Wouldn't it be nice if you could just acceptance test it in production, side-by-side with old implementations, without having the feature available? And then, if proven not to interfere with other functionality, to "release" it for early adapters? And finally, if it was proven stable and usable by the early adapters, gradually release it to everyone? Or, if proven to be disruptive, to disable (revoke) it at once without re-deployment or complex rollbacks?
I bet you would say: yes please, but no thanks. "Not possible".
I bet you: it is possible, hands down, I kid you not.
But it comes with a cost and another burden: feature administration.
In this article, we describe a possible, low-impact, relatively simple design and a possible implementation in both Java and C#. In the wrap-up conclusion, we also address some important strategies with this pattern, to prevent a big clutter-up of boolean if
-then
-else
flows in your code.
High Level Design
Looking at this model, we can see the following:
- We did not use the word "user" in the model. It is called a "Key", which is an abstraction of a "user". That allows for other type of keys then only users to switch features on.
- It uses Feature aspects to deliver the feature toggles to your code
- a feature can be either "enabled" (available to everyone), "disabled" (available to no-one) or "filtered" (available to certain users).
- A "KeyGroup" is a group of "Keys";
- A feature can have a list of included and/or excluded Keys (aka users) and/or KeyGroups (aka UserGroups).
- The feature aspects are delivered by a binary library, using a service facade instead of a client-proxy-service facade. As a consequence of that, we have chosen not to remote the feature flag administration. We simply supply a configuration file per service/component, with the needed feature flags for that service/component. If you would need that kind of centralized feature flag functionality, you would need to replace that with a remote feature service and a client concept, with probably some caching and refreshing. This local configuration file per service type of implementation is not very good suited for clustered load-balanced services, like Containers under a Kubernetes load balancer. Indeed, a form of centralization and remoting would be more applicable then.
- There is no GUI delivered for editing and toggling. Note: In modern (micro-)service oriented landscapes, a scattering of configuration files is not good for either maintenance, nor traceability. To solve that, a very pragmatic but good workable pattern is available (see Config servers in Spring Boot). Just create separate Config Git repositories with all configuration files in there, and treat configuration as "source code". So, checkin/out to your Git Config, with full versioning and traceability. All services can pull their config from there.
So, indeed, as promised, a very KISS (Keep It Stupid and Simple) straight-forward implementation, but quite easily extendable to other needs.
General Implementation Details
We have in both implementations (Java/C#) unit tests to test the code. And a small demo app MVC-ViewModel service with a tag library (in Java, Thymeleaf, in C# Tag Helpers). That is to show how the features "flow" through the chain of control: config file -> model -> service -> controller -> view -> html. And, like any modern setup, we use IOC and a container: Java Spring Boot AppContext with @Autowiring, in C# Autofac attached to the default Microsoft MVC container. I'll write an article on the how and why of IOC with Java Spring Boot and C# Autofac later, so bear with me and skip that if you're not up to speed there. Same holds for unit testing with mocking frameworks (Mockito, Fakeiteasy). An article on how and why of that is coming up as well.
I promised a KISS simple solution in both Java and C#. Here it is in outline.
Model Implementation
Regarding the FeatureFlags
model:
- 4 simple structure classes (
FeatureFlags
, Feature
, KeyGroup
, Key
) - 1 simple data file (.yml in Java, .json in C#)
- 1 simple service, that holds the
FeatureFlags
model, and does the Feature
lookup in this structure by Feature.name
- Some initialization code to parse the config data to the class model data structures (Java uses Spring
@ConfigurationProperties
, C# ConfigurationBuilder
with a little bit more setup code)
Regarding the @FeatureFlag("feature1")
annotation:
- Two annotations,
@FeatureFlag
, @FeatureFlagKey
- One Aspect:
FeatureFlagAspect
, that does the lookup of the Feature
properties in the model, through the service, and finds the Key
value in the Method
params, and does the lookup on the Tuple {<keyvalue>, <featurename>} - 1 simple service, that does the lookup in this structure:
Feature
FeatureFlagServiceImpl.getFeature(String forKey, String forFeatureName){..}
Java Implementation Details (Java 11, Spring Boot, Spring AOP, Lombok, Thymeleaf MVC)
Data Structure to Hold Feature Config
Remember, this was the model:
First, implement the configuration in this Spring Boot yaml structure according to this model:
featureFlags:
feature1:
enabled : true
included :
group1,
user3
excluded :
user4
keyGroups:
feature1 :
keys :
user1,
user2
Second, define a set of classes that match this yaml structure:
@Configuration
@ConfigurationProperties
public class FeatureFlags
{
private static final Logger log = LoggerFactory.getLogger(FeatureFlags.class);
private final Map<string, feature=""> featureFlags = new HashMap<>();
public Map<string, feature=""> getFeatureFlags()
{
log.debug("getFeatureFlags");
return featureFlags1;
}
private final Map<string, keygroup=""> keygroups = new HashMap<>();
public Map<string, keygroup=""> getKeyGroups()
{
log.debug("getKeyGroups");
return keygroups;
}
@Data
public static class Feature {
private String enabled;
private List<string> included;
private List<string> excluded;
}
@Data
public static class KeyGroup
{
private List<string> keys;
}
}
Remark 1: The "root
" keys in yaml {featureFlags
, keyGroups
} must match the {getFeatureFlags(), getKeyGroups()}
getters.
Remark 2: If you just have a list of Strings to map, use this construction, with line-separated, comma-separated list (Note: Lombok generates with @Data
the public getters/setters):
List<String> included;
included :
group1,
user3
Remark 3: Don't forget the @ConfigurationProperties
above the class structure.
Remark 4: Just @autowire FeatureFlags
as property in a class, that will parse the yaml file to the class structure:
@Service
@ConfigurationProperties
public class FeatureFlagServiceImpl implements FeatureFlagService
{
private static final Logger log = LoggerFactory.getLogger(FeatureFlagServiceImpl.class);
@Autowired
private FeatureFlags featureConfig;
...
}
This finalizes setting up the data structure parsing.
@FeatureFlag Annotation
I will not go into detail on aspect-orientation, and just give code and some remarks. See for more aspect-oriented details [1.] For this purpose, we use LTW (Load Time Weaving) with only Spring AOP.
First, define the @FeatureFlag
annotation:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FeatureFlag {
String[] features() default {""};
String paramNameForKeyOfFeature() default "";
}
Note 1: I did not implement the @FeatureFlagKey
, but use the FeatureFlag.paramNameForKeyOfFeature
as the keyName
to lookup in the method;
So, define the @Before @Aspect
:
@Aspect
@Component
public class FeatureFlagAspect
{
private static final Logger log = LoggerFactory.getLogger(FeatureAspect.class);
@Autowired
private FeatureFlagService featureConfigService;
@Before("@annotation(featureFlag)")
public void featureFlag(
final JoinPoint joinPoint,
FeatureFlag featureFlag)
throws Throwable
{
String features = Arrays.asList(featureFlag.features())
.stream()
.collect(Collectors.joining(","));
log.info("for features {} of param {}", features,
featureFlag.paramNameForKeyOfFeature());
Method method = MethodSignature.class.cast(joinPoint.getSignature()).getMethod();
log.debug("on {}", method.getName());
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature =
(MethodSignature) joinPoint.getStaticPart().getSignature();
String[] paramNames = methodSignature.getParameterNames();
Class[] paramTypes= methodSignature.getParameterTypes();
String curKey = null;
Model modelParam = null;
ModelAndView modelAndViewParam = null;
for (int argIndex = 0; argIndex < args.length; argIndex++)
{
String curParamName = paramNames[argIndex];
Object curValue = args[argIndex];
if (curParamName.equals(featureFlag.paramNameForKeyOfFeature()))
{
curKey = curValue.toString();
}
log.debug("arg {} value {}", curParamName, curValue);
if (curValue instanceof Model)
{
modelParam = (Model)curValue;
}
else if (curValue instanceof ModelAndView)
{
modelAndViewParam = (ModelAndView)curValue;
}
}
if (curKey != null)
{
for(String featureName : featureFlag.features())
{
nl.ricta.featureflag.FeatureFlags.Feature curFeature =
featureConfigService.getFeature(curKey.toString(), featureName);
if (curFeature != null )
{
if (modelParam != null)
{
modelParam.addAttribute(featureName, curFeature);
}
if (modelAndViewParam != null)
{
modelAndViewParam.addObject(featureName, curFeature);
}
}
}
}
}
Note 2: There is logic defined in the FeatureFlagService.getFeature()
, that is called from the aspect, that does the lookup in the FeatureFlags
model, of the Feature
and its properties:
@Service
@ConfigurationProperties
public class FeatureFlagServiceImpl implements FeatureFlagService
{
private static final Logger log = LoggerFactory.getLogger(FeatureFlagServiceImpl.class);
private FeatureFlags featureConfig;
@Autowired
public FeatureFlagServiceImpl(FeatureFlags featureConfig)
{
log.debug("FeatureFlagServiceImpl : #features = {}",
featureConfig.getFeatureFlags().size());
this.featureConfig = featureConfig;
}
public Map<string, feature=""> getFeatureFlags()
{
return this.featureConfig.getFeatureFlags();
}
public Feature getFeature(String forKey, String forFeatureName)
{
require(featureConfig != null);
require(forKey != null);
require(forFeatureName != null);
log.debug("getFeature, forkey={}, forFeatureName={}", forKey, forFeatureName);
Feature lookupFeature = featureConfig.getFeatureFlags().get(forFeatureName);
if (lookupFeature == null)
{
log.debug("forkey={} has no feature {}",forKey, forFeatureName);
return null;
}
if (lookupFeature.getEnabled()=="true")
{
log.debug("forkey={} enabled feature for everyone {}",forKey, forFeatureName);
return lookupFeature;
}
else if (lookupFeature.getEnabled()=="false")
{
log.debug("forkey={} enable feature for none {}",forKey, forFeatureName);
return null;
}
List<string> includedInFeaureList = lookupFeature.getIncluded();
boolean direct = includedInFeaureList.contains(forKey);
if (direct)
{
log.debug("forkey={} directly has feature {}",forKey, forFeatureName);
return lookupFeature;
}
log.debug("looking up all included keys in keygroups,
to see if key={} is included there...");
for (String included : includedInFeaureList)
{
log.debug("lookup group for {}", included);
KeyGroup lookupGroup = featureConfig.getKeyGroups().get(included);
if (lookupGroup == null)
{
log.debug("forkey={} has no feature {}",forKey, forFeatureName);
}
else
{
boolean inGroup = lookupGroup.getKeys().contains(forKey);
if (inGroup)
{
log.debug("forkey={} has feature {}",forKey, forFeatureName);
return lookupFeature;
}
}
}
log.debug("forkey={} has no feature {}",forKey, forFeatureName);
return null;
}
}
Now, we are set for the MVC and Controller part:
@FeatureFlag(features = "{feature1}", paramNameForKeyOfFeature = "user")
@GetMapping(value = "/features")
public String featuresPage(Model model, @RequestParam(value = "user") String user) {
model.addAttribute("featureEntries", featureFlagService.getFeatureFlags().entrySet());
return "featureflags/features";
}
And, the view html Features.cshtml with Thymeleaf
:
...
<div class="main">
<div>
<h2>Features</h2>
<div class='rTable' >
<div class="rTableRow">
<div class="rTableHead"><strong>Name</strong></div>
<div class="rTableHead"><span style="font-weight: bold;">Type</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Excluded</span></div>
</div>
<div class='rTableRow' th:each="featureEntry : ${featureEntries}">
<div class='rTableCell'><span th:text="${featureEntry.getKey()}"></span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getEnabled()}">
</span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getIncluded()}">
</span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getExcluded()}">
</span></div>
</div>
<div class="rTableRow">
<div class="rTableFoot"><strong>Name</strong></div>
<div class="rTableFoot"><span style="font-weight: bold;">Type</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
<div class="rTableFoot"><span style="font-weight: bold;">Excluded</span></div>
</div>
</div>
</div>
</div>
<div>
<h2>Features</h2>
<div class='rTable' >
<div class="rTableRow">
<div class="rTableHead"><strong>Name</strong></div>
<div class="rTableHead"><span style="font-weight: bold;">Type</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Excluded</span></div>
</div>
<div class='rTableRow' th:each="featureEntry : ${featureEntries}">
<div class='rTableCell'><span th:text="${featureEntry.getKey()}"></span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getEnabled()}">
</span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getIncluded()}">
</span></div>
<div class='rTableCell'><span th:text="${featureEntry.getValue().getExcluded()}">
</span></div>
</div>
<div class="rTableRow">
<div class="rTableFoot"><strong>Name</strong></div>
<div class="rTableFoot"><span style="font-weight: bold;">Type</span></div>
<div class="rTableHead"><span style="font-weight: bold;">Included</span></div>
<div class="rTableFoot"><span style="font-weight: bold;">Excluded</span></div>
</div>
</div>
</div>
</div>
...
C# DotNet Implementation Details (C# 7, .NET Core 2.1 MVC, Razor, AutoFac, Castle Core, AutoFac Dynamic Proxy)
Data Structure to Hold Feature Config
Remember, this was the model:
First, implement the configuration in a json format (featureFlagConfig.json) structure according to this model:
{
"features": [
{
"name": "feature1",
"type": "enabled",
"included": [
"group1",
"user3"
],
"excluded": [
"user4"
]
}
],
"keyGroups": [
{
"Name": "feature1",
"keys": [
"user1",
"user2"
]
}
]
}
Next, define the class structure model to hold the Feature
configuration data:
public enum FeatureType
{
enabled,
disabled,
filtered
}
public class Feature
{
public string name { get; set; }
public FeatureType type { get; set; }
public List<string> included { get; set; }
public List<string> excluded { get; set; }
}
public class KeyGroup
{
public string name { get; set; }
public List<string> keys { get; set; }
}
public class FeatureFlags
{
public Dictionary<string, feature=""> features { get; set; }
public Dictionary<string, keygroup=""> keyGroups { get; set; }
}
Next, to load this configuration json into the model, I used the .NET ConfigurationBuilder
:
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile
("featureFlagConfig.json", optional: true, reloadOnChange: true);
IConfigurationRoot config = configurationBuilder.Build();
var sectionFeatureFlags = config.GetSection("features");
List<Feature> features = sectionFeatureFlags.Get<List<Feature>>();
var sectionKeyGroups = config.GetSection("keyGroups");
List<KeyGroup> keyGroups = sectionKeyGroups.Get<List<KeyGroup>>();
FeatureFlags featureFlags = new FeatureFlags()
{
features = features.ToDictionary(f => f.name, f => f),
keyGroups = keyGroups.ToDictionary(kg => kg.name, kg => kg),
};
We also need a service, that defines the logic on the feature flag model:
public interface FeatureFlagService
{
FeatureFlags FeatureConfig { get; set; }
void LoadFeatureFlags(String fileName);
Feature getFeature(String forKey, String forFeatureName);
List<Feature> getFeatures(String forKey, List<String> forFeatureNames);
}
public class FeatureFlagServiceImpl : FeatureFlagService
{
public FeatureFlags FeatureConfig { get; set; }
public virtual void LoadFeatureFlags(String fileName)
{
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile(fileName, optional: true, reloadOnChange: true);
IConfigurationRoot config = configurationBuilder.Build();
var sectionFeatureFlags = config.GetSection("features");
List<Feature> features = sectionFeatureFlags.Get<List<Feature>>();
var sectionKeyGroups = config.GetSection("keyGroups");
List<KeyGroup> keyGroups = sectionKeyGroups.Get<List<KeyGroup>>();
this.FeatureConfig = new FeatureFlags()
{
features = features.ToDictionary(f => f.name, f => f),
keyGroups = keyGroups.ToDictionary(kg => kg.name, kg => kg),
};
}
public virtual List<Feature> getFeatures(String forKey, List<String> forFeatureNames)
{
var features = new List<Feature>();
foreach(var featureName in forFeatureNames)
{
Feature f = getFeature(forKey, featureName);
if (f != null)
{
features.Add(f);
}
}
return features;
}
public virtual Feature getFeature(String forKey, String forFeatureName)
{
Debug.WriteLine($"getFeature, forkey={forKey}, forFeatureName={forFeatureName}");
FeatureConfig.features.TryGetValue(forFeatureName, out Feature lookupFeature);
if (lookupFeature == null)
{
Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
return null;
}
if (lookupFeature.type == FeatureType.enabled)
{
Debug.WriteLine($"forkey={forKey} enabled feature for everyone {forFeatureName}");
return lookupFeature;
}
else if (lookupFeature.type == FeatureType.disabled)
{
Debug.WriteLine($"forkey={forKey} enable feature for none {forFeatureName}");
return null;
}
List<String> includedInFeaureList = lookupFeature.included;
Boolean direct = includedInFeaureList.Contains(forKey);
if (direct)
{
Debug.WriteLine($"forkey={forKey} directly has feature {forFeatureName}");
return lookupFeature;
}
Debug.WriteLine($"looking up all included keys in keygroups,
to see if key={forKey} is included there...");
foreach (String included in includedInFeaureList)
{
Debug.WriteLine($"lookup group for {included}");
FeatureConfig.keyGroups.TryGetValue(included, out KeyGroup lookupGroup);
if (lookupGroup == null)
{
Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
}
else
{
Boolean inGroup = lookupGroup.keys.Contains(forKey);
if (inGroup)
{
Debug.WriteLine($"forkey={forKey} has feature {forFeatureName}");
return lookupFeature;
}
}
}
Debug.WriteLine($"forkey={forKey} has no feature {forFeatureName}");
return null;
}
}
That finalizes setting up the data structure, feature flag logic and the parsing in C#. As promised simple, in approximately 50 lines of code.
C# [FeatureFlag] Annotation
We begin (again see [1.] Aspect-Oriented Programming and More Patterns in Micro-Services) with defining the FeatureFlag
attributes:
[AttributeUsage(
AttributeTargets.Method,
AllowMultiple = true)]
public class FeatureFlagAttribute : System.Attribute
{
public FeatureFlagAttribute(String keyName, string[] features)
{
Features = features.ToList();
KeyName = keyName;
}
public List<string> Features { get; set; } = new List<string>();
public String KeyName { get; set; } = "";
}
Then, we define the aspect (interceptor) on this annotation:
public class FeatureFlagIntersceptor : Castle.DynamicProxy.IInterceptor
{
public FeatureFlagService featureFlagService { get; set; }
public void Intercept(Castle.DynamicProxy.IInvocation invocation)
{
Debug.Print($"1. @Before Method called {invocation.Method.Name}");
var methodAttributes = invocation.Method.GetCustomAttributes(false);
FeatureFlagAttribute theFeatureFlag = (FeatureFlagAttribute)methodAttributes.Where
(a => a.GetType() == typeof(FeatureFlagAttribute)).SingleOrDefault();
if (theFeatureFlag != null)
{
var paramNameForKeyOfFeature = theFeatureFlag.ParamNameForKeyOfFeature;
ParameterInfo[] paramsOfMethod = invocation.Method.GetParameters();
int iParam;
for (iParam = 0; iParam < paramsOfMethod.Count(); iParam++)
{
ParameterInfo p = paramsOfMethod[iParam];
if (p.Name.CompareTo(paramNameForKeyOfFeature) == 0)
{
break;
}
}
string value = (string)invocation.Arguments[iParam];
List<feature> features = featureFlagService.getFeatures
(value, theFeatureFlag.Features);
Debug.Print($"2. FeatureFlagAttribute on method found with name =
{theFeatureFlag.Features}\n");
foreach(Feature f in features)
{
Debug.Print($"3. Feature {f.name} exists and type = {f.type}");
}
}
invocation.Proceed();
Debug.Print($"5. @After method: {invocation.Method.Name}\n");
}
}
And, since we need the FeatureFlag
in a REST controller to supply the feature properties to the Razor ViewModel
and HTML GUI, we also need the REST controller solution for aspect-orientation, where attribute and aspect are combined in one ActionFilterAttribute
class:
public class FeatureFlagActionAtrribute :
Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
{
public List<string> Features { get; set; } = new List<string>();
public String KeyName { get; set; } = "";
public override void OnActionExecuting
(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext context)
{
ControllerActionDescriptor actionDescriptor =
(ControllerActionDescriptor)context.ActionDescriptor;
Debug.Print($"2. @Before Method called
{actionDescriptor.ControllerName}Controller.{actionDescriptor.ActionName}");
var controllerName = actionDescriptor.ControllerName;
var actionName = actionDescriptor.ActionName;
IDictionary<object, object=""> properties = actionDescriptor.Properties;
ParameterInfo[] paramsOfMethod = actionDescriptor.MethodInfo.GetParameters();
var fullName = actionDescriptor.DisplayName;
var paramNameForKeyOfFeature = ParamNameForKeyOfFeature;
var arguments = context.ActionArguments;
string value = (string)arguments[paramNameForKeyOfFeature];
using (ILifetimeScope scope = BootStrapper.Container.BeginLifetimeScope())
{
var featureFlagService = scope.Resolve<featureflagservice>();
List<feature> features = featureFlagService.getFeatures(value, Features);
Debug.Print($"2.
FeatureFlagAttribute on method found with name = {Features}\n");
var ctrler = (Controller)context.Controller;
foreach (Feature f in features)
{
Debug.Print($"3. Feature {f.name} exists and type = {f.type}");
ctrler.ViewData[f.name] = f;
}
ctrler.ViewData["features"] =
featureFlagService.FeatureConfig.features.Values;
}
base.OnActionExecuting(context);
}
public override void OnActionExecuted
(Microsoft.AspNetCore.Mvc.Filters.ActionExecutedContext context)
{...}
}
Now, let's connect it all together, in a real life controller like we started in the article:
[FeatureFlagActionAtrribute("user", new String[] { "feature1" })]
public IActionResult DoSomethingWithFilterAction(String user)
{
Debug.Assert(ViewData["Features"] != null);
Debug.Assert(ViewData["feature1"] != null);
return View("Features");
}
And, the view html Features.cshtml with Razor:
...
<table>
<tr>
<th>enabled for user</th>
<th>type</th>
<th>included</th>
<th>excluded</th>
</tr>
@foreach (var feature in ViewData["Features"]
as IEnumerable<nl.ricta.featureflag.Feature>)
{
<tr>
<td>@feature.name <input type="checkbox" checked=@ViewData[feature.name]></td>
<td>@feature.type</td>
<td>@(string.Join(",", @feature.included));</td>
<td>@(string.Join(",", @feature.excluded));</td>
</tr>
}
</table>
...
Note 1: A feature checkbox is "checked" in the view, if the FeatureFlagActionAtrribute
feature name for that user value is "checked", so either enabled, or filtered and user value is in included list
Note 2: I use autofac as IOC container. I'll write another article on IOC, and add it to the referenced articles in due time. I enabled autofac module registration and the feature flag loading via the (singleton) FeatureFlagService
in Startup
:
public void ConfigureServices(IServiceCollection services)
{
var builder = new ContainerBuilder();
builder.RegisterModule(new FeatureFlagModule());
BootStrapper.BuildContainer(builder);
using (var scope = BootStrapper.Container.BeginLifetimeScope())
{
FeatureFlagService featureFlagService = scope.Resolve<featureflagservice>();
featureFlagService.LoadFeatureFlags("featureFlagConfig.json");
}
...
}
Conclusion and Points of Interest
What Did We Learn in this Article?
- Feature flagging allows for earlier release of functionality
- Feature flagging separates deployment from releasing functionality
- Defining and implementing the model is relatively easy, in a simple Feature flag model in a re-usable binary library, in a simple file format (.json, .yaml)
- Defining and implementing the logic is relatively easy, with a simple Feature flag logic service in a re-usable binary library
- Using aspect-oriented Feature flagging in the Apps, is basically one or two lines of code and a bit of config data, and we added a
FeatureFlag
(when all library plumbing is in place) - Resulting in a simple ViewModel feature flags where we can react on, within the MVC HTML taglibrarys (Razor, Thymeleaf, ...) in the HTML view.
As mentioned, in a clustered, loadbalanced environment, the above solution is not the best implementation(or, just plain bad and unworkable). A remote central feature flag service would be the way to go then. What needs to be added/changed in the model above? Basically, you need to adjust one piece of implementation: loading the model from that remote service, instead of via a file. That's well within a day of work. And move the model loading to a central feature flag micro-service. Okay, another day or two of work.
Strategies for Feature Flagging: Do's and Dont's
I also promised a pattern or strategy for applying Feature flagging in such a way, that the feature flag administration overhead and code clutter-up does not become too big a burden.
Do's
Actually, there are three patterns/strategies you should all apply:
- Only put a feature behind a feature flag if:
- some technical stability issues are to be expected when the feature is on
- some business and/or usability issues are to be expected when the feature is on
- you want (permanently) to dis/allow a subset of users to some feature
- Remove feature flags as soon as possible in the code (mostly, when everyone has access and the feature is proven stable and usable)
- Remove features from the code that were proven unusable for everyone.
In other words: put a feature only behind a flag if there is a very, very good reason, and then and only then; And remove it as soon as the reason is gone.
Dont's
And one strategy you should not apply:
Put everything behind a flag without good reason and let it stay there forever.
The "better save than sorry flagging" habit is a common mistake all too often made when using feature flagging, which usually comes from several "abusive" reasons:
- Nobody takes responsibility for decisions of whether a flag is needed or not, resulting in "better save than sorry flagging": developers put all new functionality behind a flag by default: "you never know.."
- "Quality excuse flagging": Quality of all features and code it so bad, that everything is put behind a flag, so you can always disable it if proven too bad.
- "Hide bad unusable functionality behind a flag": Features that were proven not usable are not removed from the code, but stay disabled behind a flag for ever, for everyone.
If you apply this very bad strategy, both feature flag administration overhead, and flagging code, will become such a burden and source of agony and pain with issues, bugs and degraded testability and usability, that it will very soon become unworkable.
Conclusion
Feature flagging can be implemented simple, and when used properly, allows for (much) faster and smoother releasing of features.
Please do!
But if feature flagging is abused for purposes it was not meant to be, it can lead to mayhem, serious agony, pain and sorrow. It will lead to degraded code, degraded testability, degraded usability and terrible quality.
Don't do that please?
History
- 16th April, 2020: Initial version