Feature Management - Dot Net

Most of the code examples below are by Nick Chapsas, his youtube video Dynamically enabling features with Feature Flags in .NET Core 3.1 was super helpful.

All the code snippets below are available at https://github.com/carlpaton/FeatureFlagDemo

What is a feature?

In its simplest form, a feature is some new behavior in your application. Having feature managment allows us to iteratively ship small changes to production which are essentially hidden / turned off. We can turn these on based on some predicate, its common to use a claim from the authenticated users JWT in the logic.

Examples

1
2
if `user-email` ends with `@gmail.com` then return true
if `user-id` is in a known collection of allowed ids then return true

Feature flags in an ASP.NET

.Net supports feature flags in the following namespaces

Config Driven

We use appsettings.json as a home for the config.

Custom Flag

This is a great way to integrate with Launch Darkly as the filter would call out to launchdarkly.com and it would manage the flag based on what you have configured.

See Custom Flag Implementation code example below for this configuration.

Simple on/off

Here NewFeatureAFlag is on and NewFeatureBFlag is off.

1
2
3
4
"FeatureManagement": {
"NewFeatureAFlag": true,
"NewFeatureBFlag": false
}

Time Window

Here HappyHolidaysFlag is on only between given time windows.

1
2
3
4
5
6
7
8
9
10
11
12
13
"FeatureManagement": {
"HappyHolidaysFlag": {
"EnabledFor": [
{
"Name": "TimeWindow",
"Parameters": {
"Start": "9 Aug 2021 09:10:00 +12:00",
"End": "9 Aug 2021 09:11:00 +12:00"
}
}
]
}
}

Percentage

Although .Net supports this out the box a percentage calculation should not really be the responsability of the application instance. If this was running in a cluster (so several nodes running in K8’s) it would make sense for the load balancer to make this decision.

1
2
3
4
5
6
7
8
9
10
11
12
"FeatureManagement": {
"RandomFlag": {
"EnabledFor": [
{
"Name": "Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}

Custom Flag Implementation

  1. Create the configuration, here the flag is BrowserFlag and the filter is BrowserFeatureFilter which configuration Parameters:AllowedBrowsers. The configuration can be anything and just needs to match
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"FeatureManagement": {
"BrowserFlag": {
"EnabledFor": [
{
"Name": "BrowserFeatureFilter",
"Parameters": {
"AllowedBrowsers": [
"Firefox"
]
}
}
]
}
}
  1. Create the options file as Features/BrowserFeatureOptions.cs, this is not automagically resolved but needs to be done using the filters context.
1
2
3
4
5
6
7
8
namespace FeatureFlagDemo.Features
{
public class BrowserFeatureOptions
{
public string[] AllowedBrowsers { get; set; }
}
}

  1. Create the the filter as Features/BrowserFeatureFilter.cs, the alias BrowserFeatureFilter needs to match the name in the appsettings.json configuration. The interface IFeatureFilter comes from the namespace Microsoft.FeatureManagement and has a single method EvaluateAsync which will be called by the framework.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.FeatureManagement;
using System.Linq;
using System.Threading.Tasks;

namespace FeatureFlagDemo.Features
{
[FilterAlias("BrowserFeatureFilter")]
public class BrowserFeatureFilter : IFeatureFilter
{
private readonly IHttpContextAccessor _httpContextAccessor;

public BrowserFeatureFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
var settings = context.Parameters.Get<BrowserFeatureOptions>();

return Task.FromResult(settings.AllowedBrowsers.Any(userAgent.Contains));
}
}
}

  1. Dependency injection

Add a default implementation for the Microsoft.AspNetCore.Http.IHttpContextAccessor this is the httpContextAccessor instance injected into the filter.

1
2
3
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();

Finally resolve the feature managment services

1
2
3
4
services.AddFeatureManagement()
.AddFeatureFilter<BrowserFeatureFilter>()
.AddFeatureFilter<Foo2Filter>()
.AddFeatureFilter<Foo3Filter>();
  1. Validate the flag BrowserFlag in code

Controller action result annotation

If the flag passes then the endpoint will be served else it will return 404 Not found.

1
2
3
4
5
6
7
8
9
10
11
namespace FeatureFlagDemo.Controllers
{
public class HomeController : Controller
{
[FeatureGate("BrowserFlag")]
public IActionResult BrowserNewFeature()
{
return View();
}
}
}

Using tag helpers in the view

1
2
3
4
5
6
7
@using Microsoft.FeatureManagement
@inject IFeatureManager FeatureManager
@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

<feature name="BrowserFlag">
<h2>You can see BrowserFlag things!</h2>
</feature>

Using conditions in the view

1
2
3
4
@if (await FeatureManager.IsEnabledAsync("BrowserFlag"))
{
<h2>You can see BrowserFlag things!</h2>
}

References