Andrew Lock | .NET Escapades

Andrew Lock

in ASP.NET Core Feature Flags Configuration ~ 5 min read.

Filtering action methods with feature flags
Adding feature flags to an ASP.NET Core app - Part 2

In the first post in this series, I introduced the Microsoft.FeatureManagement library, and showed how to use it to add feature flags to an ASP.NET Core app.

In this post, I introduce the companion Microsoft.FeatureManagement.AspNetCore library. This library adds ASP.NET Core-specific features for working with feature flags, such as Tag Helpers and Action Filters.

Microsoft.FeatureManagement was introduced in a recent .NET Community Standup and is currently in preview, so some details may change when it's properly released.

ASP.NET Core-specific feature flag integration

As I described in my previous post, the Microsoft.FeatureManagement library is a .NET Standard 2.0 library that builds on top of the Microsoft.Extensions.Configuration. It provides a standardised way of adding feature flags to an application.

There's nothing ASP.NET Core-specific in this base library, so you're free to use it in any .NET Standard 2.0 app. However, there's an additional library, Microsoft.FeatureManagement.AspNetCore, that does depend on ASP.NET Core, which contains various helper functions and extension methods for working with feature flags. These are described in the documentation (the library isn't open-sourced yet), and include some of the following:

In this post, I'll show how to use MVC action filters to conditionally remove MVC actions, the FeatureTagHelper to hide sections of the UI, and an IDisabledFeaturesHandler to provide custom behaviour if a user attempts to access an action hidden behind a feature flag.

Setting up the project

I'm going to start with a simple ASP.NET Core application, the same as in the last post. If you've already tried out the process from that post, you can just replace the reference to Microsoft.FeatureManagement with a reference to Microsoft.FeatureManagement.AspNetCore.

dotnet add package Microsoft.FeatureManagement.AspNetCore --version 1.0.0-preview-008560001-910

After adding it, your .csproj should look something like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="1.0.0-preview-008560001-910" />
  </ItemGroup>

</Project>

Next, register the required services in Startup.ConfigureServices():

public class Startup 
{
    public void ConfigureServices(IServiceCollection services)
    {
        //...
        services.AddFeatureManagement();
    }
}

And add the necessary configuration for a simple boolean feature flag Beta to appsettings.json

{
  "FeatureManagement": {
    "Beta": false
  }
}

Finally, create a static helper class for referencing your feature in code:

public static class FeatureFlags 
{
    public const string Beta = "Beta";
}

That's the basic project configured, so now we'll use some of the features provided by Microsoft.FeatureManagement.AspNetCore.

Removing an MVC action using the [Feature] attribute

Let's imagine you have a "traditional" MVC controller (as opposed to a Razor Page) that you only want to be available when the Beta feature flag is enabled. You can decorate the controller (or specific actions) with the [Feature] attribute, providing the name of the feature as an argument:

using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement;

[Feature(FeatureFlags.Beta)] // Beta feature flag must be enabled
public class BetaController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

If you try to navigate to this page (/Beta) when the feature is enabled, you'll see the View rendered as expected:

Viewing the beta page

However, if the Beta feature flag is disabled, you'll get a 404 when trying to view the page:

Viewing the beta page when disabled gives a 404

It's as though the controller doesn't exist at all. That's maybe not the best experience from the user's perspective, so we'll customise this shortly.

The [Feature] attribute is a standard action filter, so you can apply it to action methods, controllers, base controllers, or globally. However, there's a couple of things to be aware of:

  • The [Feature] attribute takes an array of feature flags, in its constructor. If any of those features are enabled, the controller is enabled. Put another way, the filter does an OR check of the features, not an AND.
  • The [Feature] attribute is an IActionFilter not an IPageFilter, so it currently doesn't work on Razor Pages. That seems like an oversight that will surely be fixed before the final release.

Custom handling of missing actions

As shown in the previous section, if an action is removed due to a feature being disabled, the default is to generate a 404 response. That may be fine for some applications, especially if you're using error handling middleware to customise error responses to avoid ugly "raw" 404.

However, it's also possible that you may want to generate a different response in this situation. Maybe you want to redirect users to a "stable" page, return a "join the waiting list" view, or simply return a different response, like a 403 Forbidden.

You can achieve any of these approaches by creating a service that implements the IDisabledFeaturesHandler interface. Implementers are invoked as part of the action filter pipeline, when an action method is "removed" due to a feature being disabled. In the example below, I show how to generate a 403 Forbidden response, but you have access to the whole ActionExecutingContext in the method, so you can do anything you can in a standard action filter:

public class RedirectDisabledFeatureHandler : IDisabledFeaturesHandler
{
    public Task HandleDisabledFeatures(IEnumerable<string> features, ActionExecutingContext context)
    {
        context.Result = new ForbidResult(); // generate a 403
        return Task.CompletedTask;
    }
}

To register the handler, update your call to AddFeatureManagement() in Startup.ConfigureServices():

public class Startup 
{
    public void ConfigureServices(IServiceCollection services)
    {
        //...
        services.AddFeatureManagement()
            .UseDisabledFeaturesHandler(new RedirectDisabledFeatureHandler());
    }
}

With the handler registered, if you now try to access a disabled feature, a 403 response is generated, which is intercepted by the error handling middleware, and you're redirected to the "Access Denied" page for the app:

Instead of a 404, you're redirect to a

With these features you can disable action methods based on whether a feature is enabled and have fine control over what happens when a disabled feature is accessed. Ideally however, users shouldn't be attempting to call actions for disabled features.

Generally speaking, you don't want your users to be seeing "Access Denied" pages due to trying to access disabled features. Instead, you should hide the feature-flag-gated functionality in the UI as well.

As with all validation, you can't rely on hiding things client-side. Always use server-side feature flag checks; only hide content in the UI to give a better user experience.

One way to hide sections in Views would be to inject the IFeatureManager service into views using dependency injection. For example, imagine you want to add a link to the BetaController in the navigation bar of your default layout. You could use the @inject directive, and check for the feature manually:

<!-- Inject the service using DI  -->
@inject  Microsoft.FeatureManagement.IFeatureManager _featureManager; 

<nav>
    <ul>
        <!-- Check if the feature is enabled  -->
        @if (_featureManager.IsEnabled(FeatureFlags.Beta))
        {
            <li class="nav-item">
                <a class="nav-link text-dark" asp-controller="Beta" asp-action="Index">Beta</a>
            </li>
        }
    </ul>
</nav>

This approach works fine, but for this example, you could also use the FeatureTagHelper to achieve the same thing but with cleaner markup:

<nav>
    <ul>
        <!-- Check if the feature is enabled using FeatureTagHelper -->
        <feature name="@FeatureFlags.Beta">
            <li class="nav-item">
                <a class="nav-link text-dark" asp-area="" asp-controller="Beta" asp-action="Index">Beta</a>
            </li>
        </feature>
    </ul>
</nav>

The FeatureTagHelper works for this simple use-case, where you have a single feature flag that must be enabled to show some UI. Unfortunately, if you want to do anything more complicated than that (show different UI if the feature is disabled, check for multiple features) then you'll have to fallback to injecting IFeatureManager.

As mentioned in the .NET community Standup, the team are aware of the limitations with the TagHelper - hopefully they'll be addressed before it's fully released. Personally I wish they would just open source it already, as fixing these things would be easy for the community to do pretty quickly.

That covers the main MVC features exposed in the Microsoft.FeatureManagement.AspNetCore library. So far, I've only demonstrated creating simple feature flags that are just boolean values. In the next post I'll show how you can create more complex (and more interesting!) feature flags by using feature filters. These allow you to customise the feature flags based on the current request, which opens the door to more advanced scenarios.

Summary

The Microsoft.FeatureManagement.AspNetCore library builds on the features in Microsoft.FeatureManagement, adding ASP.NET Core helpers for working with features flags. It contains action filters for disabling actions that are behind feature flags, tag helpers for conditionally hiding UI elements, and extension methods for customising the ASP.NET Core pipeline based on the enabled features. In the next post I'll show some of the more powerful features of the library that let you use more complex feature flags.

Loading comments powered by Disqus, please wait…
Andrew Lock | .Net Escapades

Stay up to the date with the latest posts!

Oops! Check your details and try again.
Thanks! Check your email for confirmation.