Feature Flagging with .NET Feature Management
Handling feature flags using a simple .NET library
Photo by Eury Escudero on Unsplash
The ability to turn features on and off in your application is powerful for many reasons such as:
Supporting CI/CD and Quicker Release Cycles
- Feature flags allow you to merge earlier with the confidence of hiding incomplete or experimental features. This supports trunk-based development or similar branching strategies well. You don't have to necessarily wait for an official launch to have your feature deployed to production. This promotes quicker release cycles.
A/B Testing
- They facilitate A/B testing by allowing you to target different user groups to by providing variations of features.
Reducing risk
- Feature flags provide a comfortable safety net (mostly) since you can just "turn it off" without necessarily having any downtime.
Controlled Production Testing / Canary Releases
- Feature flags make this possible similar to A/B testing. You are able to target a set of users to verify the behaviour in production.
Microsoft has a useful package for feature management called .NET Feature Management. It is simple to use and easy to understand. Follow along as I create a simple API that returns products but has its behaviour altered depending on feature flags.
First you need to add the .NET Feature Management package to your project:
Install-Package Microsoft.FeatureManagement
Then it as simple as registering the feature management service in your Program.cs.
builder.Services.AddFeatureManagement();
In this sample I have used the standard implementation described in the documentation. By convention, feature flags are defined in the FeatureManagement configuration section but you do have the ability to change this as described in the documentation.
I will be changing the behaviour of finding products based on my feature flags. My product model is defined below:
public class Product
{
public string Name { get; set; }
public int Quantity { get; set; }
public bool IsLocallyProduced { get; set; }
}
Now we can setup a few feature toggles. This is what my appsettings file looks like:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"FeatureManagement": {
"ShowLocallyProducedItemsOnly": false,
"ShowItemsWithLowStock": false,
"LimitSearchResults": false
}
}
ShowLocallyProducedItemsOnly -> This will return products that have IsLocallyProduced set to true.
ShowItemsWithLowStock -> By default we only show items with a Quantity \> 5. This will allow us to see all products irrespective of their quantity.
LimitSearchResults -> This will limit the result set to only 5 products.
The above 3 features are very simple and are intended to articulate the impact that feature flags could have.
I've defined a static class to reference my feature flag names for ease of use:
public static class Feature
{
public const string ShowItemsWithLowStock = "ShowItemsWithLowStock";
public const string LimitSearchResults = "LimitSearchResults";
public const string ShowLocallyProducedItemsOnly = "ShowLocallyProducedItemsOnly";
}
The package provides an IFeatureManager interface to interact with the feature manager. In my example I use the feature manager in my ProductService:
private readonly IFeatureManager _featureManager;
public ProductService(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
IFeatureManager has a method for checking if a feature is enabled based on your configuration. This will allow you to control the behaviour of your application depending on the configured values.
await _featureManager.IsEnabledAsync("FeatureName")
As an example I use the above method to change the behaviour of my FindProductsAsync method:
public async Task<IEnumerable<Product>> FindProductsAsync()
{
var products = GetDummyProductData();
if (await _featureManager.IsEnabledAsync(Feature.LimitSearchResults)) products = products.Take(5);
if (!await _featureManager.IsEnabledAsync(Feature.ShowItemsWithLowStock))
products = products.Where(p => p.Quantity > 5);
if (await _featureManager.IsEnabledAsync(Feature.ShowLocallyProducedItemsOnly))
products = products.Where(p => p.IsLocallyProduced);
return products;
}
It is as simple as that to implement feature flagging in your application. Now let's see how that affects my application.
With all of my settings set to false I get the following result from my endpoint:
[
{
"name": "A",
"quantity": 10,
"isLocallyProduced": true
},
{
"name": "L",
"quantity": 20,
"isLocallyProduced": false
},
{
"name": "J",
"quantity": 15,
"isLocallyProduced": false
},
{
"name": "M",
"quantity": 10,
"isLocallyProduced": false
},
{
"name": "O",
"quantity": 10,
"isLocallyProduced": false
},
{
"name": "U",
"quantity": 10,
"isLocallyProduced": false
}
]
As you can see I receive all items whether they are produced locally or not, only items that have adequate quantity and I have more than 5 items.
So what happens if I set ShowLocallyProducedItemsOnly in my appsettings?
I am returned only items that have this flag as true:
[
{
"name": "A",
"quantity": 10,
"isLocallyProduced": true
}
]
I didn't need to restart my application for this and the feature manager took care of reading the values for me. That is super useful.
The feature manager has other useful filters that extend beyond just a simple true or false. You can also use the following filters:
TimeWindow -> Contains a start and end time in which the feature would be turned on. For example I'm going to test the LimitSearchResults feature for a specified time only:
"FeatureManagement": {
"ShowLocallyProducedItemsOnly": true,
"ShowItemsWithLowStock": false,
"LimitSearchResults": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "Wed, 05 June 2024 13:59:59 GMT",
"End": "Mon, 10 June 2024 00:00:00 GMT"
}
}
]
}
}
Percentage -> Randomly shows a percentage of users the feature:
"FeatureManagement": {
"ShowLocallyProducedItemsOnly": true,
"ShowItemsWithLowStock": false,
"LimitSearchResults": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": "50"
}
}
]
}
}
You also have the ability to chain rules. So you can specify when all rules should be matched or any rules should be matched. This is done by using the RequirementType property:
"FeatureManagement": {
"ShowLocallyProducedItemsOnly": true,
"ShowItemsWithLowStock": false,
"LimitSearchResults": {
"RequirementType": "All",
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "Mon, 01 May 2023 13:59:59 GMT",
"End": "Sat, 01 Jul 2023 00:00:00 GMT"
}
},
{
"Name": "Percentage",
"Parameters": {
"Value": "50"
}
}
]
}
}
You can find the detailed schema for FeatureManagement here.
The FeatureManager extends even further to prevent/allow executions on controllers using FeatureGates:
[FeatureGate("FeatureX")]
public class HomeController : Controller
{
}
Feature flagging is really powerful and has many benefits to offer. As usual, you need to be responsible with any implementation and it doesn't give you an excuse to merge for the sake of merging :)
The library has much more to offer including creating customised filters, targeting users and more. You can play around with my test solution on GitHub and explore the library in much more detail.