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>
Advertisements

Localising MVC Views using Display Modes

When considering localisation/globalisation/internationalisation of an MVC website there are a lot of routes you can take. The easiest of these (and therefore the most tempting) is to use a Resources.resx file to handle string replacement, but there are scenarios where you need to do more than just replace some text.

You might need to rearrange the page to handle right-to-left languages, you might need to change a background image that is particularly offensive in some countries; either way, you’re looking at writing a specific view for a specific culture.

Custom Per-Culture Views

While googling for a solution I found quite a few examples (such as this blog post from Brian Reiter) that work something like this:

  1. Create a folder structure like the one below for your culture-specific views
  2. Subclass WebFormViewEngine or RazorViewEngine to transform the view path to the new folder structure

In the example above, any visitor with a culture of en-GB will see /Views/i18n/en-GB/Home/Index.cshtml; anyone else will see the default view.

In many ways this is a very elegant solution: it doesn’t require you to create localised versions of every view; it elegantly falls back to sensible defaults; and you don’t have to write a lot of custom code. Unfortunately it does have a couple of problems:

  • It requires that you subclass a specific -ViewEngine implementation
  • It doesn’t support localised versions of master pages
  • It doesn’t work with the MVC mobile features introduced in MVC4

The first is not necessarily a show-stopper, but the mobile support issue is a pretty big deal for my scenario and the lack of master page localisation could cause problems.

“Borrowing” from Mobile Features

Seeing as the mobile features were the main driver behind my need to change, it makes sense to use the same mechanism for path-transformation that they use: display modes.

Display Modes (Simple Version)

Display modes allow you to set up special suffixes (suffices?) such as “Mobile” that, under certain circumstances, ASP.NET will look for before looking for the standard .cshtml. For example, the following would set up an iPhone suffix that would be considered whenever the user agent of a request includes the string “iPhone”:

DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("iPhone")
{
    ContextCondition = (context => context.Request.UserAgent
        .IndexOf("iPhone", StringComparison.OrdinalIgnoreCase) >= 0)
});

This means that when a user browses to your site using an iPhone, ASP.NET will try to find a /Views/Controller/Index.iPhone.cshtml before falling back to /Views/Controller/Index.cshtml.

Display Modes (Slightly-More-Detailed Version)

So how does the example above actually work?

The DefaultDisplayMode class in the example is part of MVC and is responsible for the transformation of a virtual path and for checking if it exists. By adding it to the DisplayModeProvider.Instance.Modes collection at index zero, we are telling ASP.NET that it should check this display mode before any others.

To determine which mode to use, each IDisplayMode instance that is added to the DisplayModeProvider.Instance.Modes collection will have the GetDisplayInfo method called in the order that they are added.

public DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func<string, bool> virtualPathExists)
{
	//...
}

If the instance is able to return a valid view path then it does so by returning an instance of DisplayInfo; if not, it returns null and the next element in the Modes collection is queried.

So how can we use this to handle localisation?

Handling Localisation with Display Modes

The first step is to subclass DefaultDisplayMode so we have somewhere to implement our localisation path transformation. We could implement IDisplayMode directly, but we get some things for free by using the existing implementation:

public class LocalisedDisplayMode : DefaultDisplayMode
{
	private string _suffix;

	public LocalisedDisplayMode(string suffix)
		: base(suffix)
	{
		_suffix = suffix;
	}
}

All we are doing here is accepting a suffix in the constructor that we pass to the base class as well as keeping a reference for ourselves – we’ll need it later.

Next up we want to override the implementation of GetDisplayInfo so that we can transform the path to the localised equivalent.

public override DisplayInfo GetDisplayInfo(
	HttpContextBase httpContext,
	string virtualPath,
	Func<string, bool> virtualPathExists)
{
    //...	
}

The 3 parameters are:

  • The context of the current request
  • The current virtual path
  • An anonymous helper function to determine whether or not a given virtual path exists

Our implementation needs to:

  1. Get the appropriate culture for the user
  2. Check to see if a custom view exists for the specific culture (e.g. en-GB)
  3. If not, check to see if a custom view exists for the 2-character culture (e.g. en)
  4. If not, return null to indicate that this display mode cannot handle the request

Acquiring User Culture

We can get an appropriate culture for the user through a number of mechanisms – Accept-Language headers/Request.UserLanguages, storing a cookie, using CultureInfo.CurrentUICulture etc. – and how this is sourced doesn’t really matter for this example. For now, I’m going to leave it as an abstract method:

protected abstract CultureInfo GetCulture();

Selecting a View Path

Let’s assume that a request for ~/Views/Home/Index.cshtml is made from a user with a culture of es-ES, and our display mode has been set up with a suffix of “suffix”. We want to check the following paths (in order) to see if they exist:

  • ~/Views/i18n/es-ES/Home/Index.suffix.cshtml
  • ~/Views/i18n/es/Home/Index.suffix.cshtml

We’re not worried about looking in /Shared or in checking the non-localised paths – those will be covered by default display modes.

public override DisplayInfo GetDisplayInfo(
	HttpContextBase httpContext,
	string virtualPath,
	Func<string, bool> virtualPathExists)
{
	var culture = this.GetCulture();

	return
		TryGetDisplayInfo(virtualPath, culture.Name, virtualPathExists) ??
		TryGetDisplayInfo(virtualPath, culture.TwoLetterISOLanguageName, virtualPathExists);
}

private DisplayInfo TryGetDisplayInfo(string virtualPath, string cultureString, Func<string,bool> virtualPathExists)
{
	//use the base class to handle suffix transformation
	var transformedPath = this.TransformPath(virtualPath, _suffix);
		
	//use a regular expression to replace the view folder
	var cultureSpecificPath = Regex.Replace(
				transformedPath,
				"^~/Views/",
				string.Format("~/Views/i18n/{0}/", cultureString));

	//return the virtual path if it exists...
	if (virtualPathExists(cultureSpecificPath))
		return new DisplayInfo(cultureSpecificPath, this);

	//...or null if it doesn't
	return null;
}

Here we have implemented a helper method that will transform and check a path, then return a DisplayInfo instance if it exists. The null-coalescing operator in GetDisplayInfo will fallback to each less-specific view path in turn.

Plugging it in

The final step is to insert the new display mode into the configuration. We do this in the Global.asax file as below:

protected void Application_Start()
{
	DisplayModeProvider.Instance.Modes.Insert(0, new LocalisedDisplayMode(DisplayModeProvider.DefaultDisplayModeId));
	DisplayModeProvider.Instance.Modes.Insert(0, new LocalisedDisplayMode(DisplayModeProvider.MobileDisplayModeId)
		{ ContextCondition = context => context.GetOverriddenBrowser().IsMobileDevice });

	//...
}

We are inserting 2 instances so we can mimic the behaviour of the 2 default display modes (Default and Mobile), and we are inserting them at the top of the list so that they take precedence over the defaults.

Now we can create localised versions of master pages, views, partial views, mobile views…pretty much everything!

As an added bonus, this implementation is reusable between both Razor and Web Forms, and is much easier to unit test than the subclassed-view-engine approach.