Conditional HTML Rendering with Microsoft Feature Flags (Microsoft.FeatureManagement)

This is part seven in a series of articles.

You can render HTML in your views based on whether or not a feature flag is enable or disabled. To do this you can make use of the FeatureTagHelper.

For example suppose the following Printing feature is configured to be off in the appsettings.json:

"FeatureManagement": {
  "Printing": false    
}

Now in the view (or _ViewImports.cshtml) you can add the tag helper and then use the <feature> tag:

@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <feature name="Printing">
        <button>Print</button>
    </feature>
</div>

Notice in the preceding view that the <feature> tag has the  name property set to the feature set in config, i.e. “Printing”.

If you have defined your features in an enum as described earlier in this series, you could use a nameof expression instead:

@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <feature name="@nameof(Features.Printing)">
        <button>Print</button>
    </feature>
</div>

The FeatureTagHelper has a number of ways it can be used. For example you can invert the configured feature value by setting the negate property to true:

@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

    <div class="text-center">
        <h1 class="display-4">Welcome</h1>
        <feature name="@nameof(Features.Printing)">
            <button>Print</button>
        </feature>
        <feature name="@nameof(Features.Printing)" negate="true">
            <button disabled>Printing is disabled</button>
        </feature>
    </div>

The preceding view will show a disabled button when the printing feature is not available.

You can also specify multiple features in a single <feature> name:

<feature name="Printing, PrintPreview">
    <button>Print</button>
</feature>

Notice in the preceding view that both the Printing and PrintPreview features are used (comma separated) in the name attribute. By default this is a logical AND so both features need to be enabled for the contents of the <feature> tag to be rendered, e.g.:

"FeatureManagement": {
  "Printing": true,
  "PrintPreview": true
}

If you want to use a logical OR and render the HTML when any of the specified features are enabled you can set the requirement attribute to “Any”:

@addTagHelper *, Microsoft.FeatureManagement.AspNetCore

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <feature name="Printing, PrintPreview" requirement="Any">
        <button>Print</button>
    </feature>
</div>

Now if either (or both) Printing or PrintPreview is enabled the button will be rendered.

You could also implement a more strongly typed tag helper that uses a Features enum to represent the features:

public enum Features
{
    Printing,
    PrintPreview,
    QuickQuotes,
    OnlineChat
}
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.FeatureManagement;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
    /// <summary>
    /// Based on https://github.com/microsoft/FeatureManagement-Dotnet/blob/master/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs
    /// </summary>    
    public class FeatureTagHelper : TagHelper
    {
        private readonly IFeatureManager _featureManager;

        public Features Feature { get; set; }
        
        public FeatureTagHelper(IFeatureManagerSnapshot featureManager)
        {
            _featureManager = featureManager;
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            DontOutputTagName();

            bool isFeatureEnabled = await _featureManager.IsEnabledAsync(Feature.ToString()).ConfigureAwait(false);

            if (!isFeatureEnabled)
            {
                output.SuppressOutput();
            }

            void DontOutputTagName() => output.TagName = null;
        }
    }
}

@addTagHelper *, WebApplication1

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <feature feature="@Features.Printing">
        <button>Print</button>
    </feature>
</div>

You could also go one step further and define a tag helper for every feature:

using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.FeatureManagement;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
    /// <summary>
    /// Based on https://github.com/microsoft/FeatureManagement-Dotnet/blob/master/src/Microsoft.FeatureManagement.AspNetCore/TagHelpers/FeatureTagHelper.cs
    /// </summary>    
    public abstract class FeatureTagHelper : TagHelper
    {
        private readonly IFeatureManager _featureManager;

        protected Features _feature;
        
        public FeatureTagHelper(IFeatureManagerSnapshot featureManager, Features feature)
        {
            _featureManager = featureManager;
            _feature = feature;
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            DontOutputTagName();

            bool isFeatureEnabled = await _featureManager.IsEnabledAsync(_feature.ToString()).ConfigureAwait(false);

            if (!isFeatureEnabled)
            {
                output.SuppressOutput();
            }

            void DontOutputTagName() => output.TagName = null;
        }
    }

    public class PrintingTagHelper : FeatureTagHelper
    {
        public PrintingTagHelper(IFeatureManagerSnapshot featureManager) : base(featureManager, Features.Printing) {}        
    }

    public class OnlineChatTagHelper : FeatureTagHelper
    {
        public OnlineChatTagHelper(IFeatureManagerSnapshot featureManager) : base(featureManager, Features.OnlineChat) {}
    }
}

@addTagHelper *, WebApplication1

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <printing>
        <button>Print</button>
    </printing>
    <online-chat>
        <button>chat</button>
    </online-chat>
</div>

SHARE:

Add comment

Loading