ASP.NET Core Feature Flag TagHelper

Releasing stuff is dangerous: you might break things, you might annoy your users or you could screw up in any number of entertaining ways.

Feature flags are a great way to get functionality into production without quite so much risk. You can release your new feature to only a small subset of your users and then roll it out once you’re happy that things aren’t on fire.

The way you define flags will depend on the requirements for roll out - sometimes a configuration setting is sufficient; sometimes you’ll need per-user settings or something more complex. That’s outside the scope of this article though - I’ll leave that part up to you.

Once you have your flags defined you want to start modifying content based on those flags. You coulds do that with a bunch of if statements but when we’re talking about Razor it can get messy fast.

Instead, wouldn’t it be nice to wrap your new stuff in a special tag?

<feature flag="MyCoolNewThing">
  <!-- cool content here -->
</feature>

Or if you want to display something only for users without the new feature?

<feature flag="MyCoolNewThing" disabled>
  Click here to enable my new cool thing!
  <button>Enable Now!</button>
</feature>

Tag Helpers make this easy!

The TagHelper Class

Tag helpers allow you to write server-side code that manipulates the DOM during the render of a Razor view. They can accept dependency-injected services (in the scope of the current request) and can access attributes and child content of their Razor element.

They are implemented by extending the TagHelper class and are used in Razor views by converting the class name to a kebab-cased equivalent. e.g. MyGreatNewTagHelper would be available via the my-great-new tag.

The class then modifies the generated DOM by overriding either the Process or ProcessAsync methods. These methods are passed context objects to allow both interrogation of the current content and modification of the output.

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
  //...
}

Hiding Child Content

In our case, the “modification” we want is really simple: we want to optionally hide all child content. This is directly supported via the TagHelperOutput.SuppressContent() method so we can hide content with a 1 line method:

public override async Task ProcessAsync(
  TagHelperContext context,
  TagHelperOutput output)
{
  output.SuppressContent();
}

note that we don’t need to be using the ProcessAsync variant as we have no async code yet, but we’ll be adding some shortly

Optionally Hiding Child Content

We only want to hide the child content if a feature flag is disabled, so we need to know the state of the flag. There are a lot of ways that feature flag settings could be implemented (configuration settings, per-user flags, etc.) so we are going to abstract all of that away behind an interface:

public interface IFeatureFlagProvider {
  Task<bool> IsEnabled(FeatureFlag featureFlag);
}

public enunm FeatureFlag {
  Unknown,
  MyCoolNewThing,
  AnotherAwesomeFeature,
  Etc
}

The IFeatureFlagProvider accepts an enum value identifying a feature and asynchronously returns a boolean indicating whether or not the feature is enabled. Any complexity around how you determine the availability of the feature can happily hide behind this facade.

Note: I’m using an enum to define my features but strings are a valid alternative. I prefer enums because you can find all references easily and if you’re adding new features then you’re going to be changing code anyway!

As I said above, tag helpers can accept injected dependencies so as soon as we have registered an implementation of IFeatureFlagProvider we can use it in our helper. We’re also going to add a Flag property which will be set on each instance of the tag.

public class FeatureTagHelper : TagHelper
{
  private readonly IFeatureFlagProvider _featureFlagProvider;

  public FeatureTagHelper(IFeatureFlagProvider featureFlagProvider)
  {
    _featureFlagProvider = featureFlagProvider;
  }

  public FeatureFlag Flag { get; set; }

  public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
  {
    var isFeatureEnabled = await _featureFlagProvider.IsEnabled(this.Flag);
    if (!isFeatureEnabled)
      output.SuppressContent();
  }
}

Support Disabled State

We may want to show content only when a feature is not enabled (e.g. an “enable now” message) so we want to support the inverse. One option would be to create a second tag helper but I felt like a disabled flag made more sense when in Razor:

<feature flag="FeatureName" disabled>...</feature>

To achieve this we can use the supplied TagHelperContext to look for a disabled attribute and then combine this with our isFeatureEnabled condition from earlier:

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    var isFeatureEnabled = await _featureFlagProvider.IsEnabled(this.Flag);
    var isTagDisabled = context.AllAttributes.Where(a => a.Name?.ToLowerInvariant() == "disabled").Any();

    var showContent = isFeatureEnabled != isTagDisabled;
    if (!showContent)
      output.SuppressOutput();
  }
}

Using the TagHelper

Now that we’ve created the tag helper we can start using it in our Razor views.

Before it becomes available we will need to add it to the project in the Views/_ViewImports.cshtml file that is created as part of the ASP.NET Core templates.

You can either add all tag helpers in your project or add each one individually.

<!-- add all in assembly MyProject -->
@addTagHelper *, MyProject
<!-- or add individually -->
@addTagHelper MyProject.TagHelpers.FeatureTagHelper, MyProject

Once that’s done you can use the new tags in any of your Razor pages or views as below:

<feature flag="MyCoolNewThing">
  <!--
    content will only be displayed if FeatureFlag.MyCoolNewThing is enabled
    for the current user
  -->
  <h1>Cool New Thing</h1>
</feature>

<feature flag="MyCoolNewThing" disabled>
  <!--
    content will only be displayed if FeatureFlag.MyCoolNewThing is disabled
    for the current user
  -->
  Click here to enable my new cool thing!
  <button>Enable Now!</button>
</feature>