Gradually Rollout New Features with Targeting Feature Flags (Microsoft.FeatureManagement)

This is part nine in a series of articles.

One of the feature filters that Microsoft Provides is the targeting feature filter. This allows you to gradually rollout a feature to make sure it’s working for a small subset of users before rolling it out to everyone. This approach can help to find bugs that might have slipped into the feature during development. By not just turning on the feature for all users, you can shield them from any problems, only a few users will see the error.

At first glance the targeting feature filter might seem a bit confusing but essentially it allows you to expose the new feature to your “audience”. The “audience” are your users.

Your audience can be specified in 3 ways:

  1. By specific user(s)
  2. By which group(s) the current user belongs to
  3. By a percentage of all users, regardless of 1 & 2 above

For example, you could release a new feature and only have it enabled for 1 or 2 specific users (e.g. Sarah and Amrit). These users could be part of the development team that have accounts setup in the production system. Sarah and Amrit will see the new feature and be able to check it is working, everyone else won’t see it.

Once Sarah and Amrit have used the new feature and are sure that there are no errors, the feature can be rolled out to a small subset of users. This subset is represented as a group. For example you could have a subset of users (that all belong to a group called “earlyadopters”) who have opted in to get access to “beta” features before other users. These users would be made aware at the time they opted in that they might experience some occasional errors. You could  enable the new feature for all “earlyadopters” or for a subset of them based on a percentage. For example you could start with 10% of early adopters and if there are no errors, gradually increase it to 100% of early adopters.

Once all early adopters are using the feature and there is confidence that the new feature is working correctly, the feature can be rolled out to all the other users. You could do this all at once, or once again start by rolling it out to 50% of them and gradually increase to 100%, or just go straight to 100% and enable for everyone.

Essentially, feature targeting gives you a great amount of control on how you roll out new features. Contrast this with just releasing a new feature to production with no feature flags and all users starting to use it at the same time. If there is a problem with the new feature, all users will will see it and be affected by it.

Using Feature Targeting

Create an ASP.NET Core 3.1 MVC app and create it with “individual user accounts” authentication. This will enable you to add users to local SQL database rather that relying on Windows auth for example.

The first thing to do is map users and groups in whatever authentication method is being used to users/groups in the Microsoft Feature Management world. Specifically to create TargetingContext instances for users. A TargetingContext contains the user id and list of groups to which the user belongs. This information is used to position the current user in the audience that has been configured.

One way to build a TargetingContext is to get it from the current HTTP context by getting the HttpContext.User property.

To create TargetingContexts, you can create a class that implements the Microsoft.FeatureManagement.FeatureFilters.ITargetingContextAccessor interface. This interface has a method GetContextAsync inside which you create a TargetingContext for the current user:

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.FeatureManagement.FeatureFilters;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;

namespace WebApplication2.Models
{
    /// <summary>
    /// Based on https://github.com/microsoft/FeatureManagement-Dotnet/blob/master/examples/FeatureFlagDemo/HttpContextTargetingContextAccessor.cs
    /// </summary>
    public class HttpTargetingContextAccessor : ITargetingContextAccessor
    {
        private const string CacheKey = "HttpContextTargetingContextAccessor.TargetingContext";
        private readonly IHttpContextAccessor _httpContextAccessor;

        public HttpTargetingContextAccessor(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public ValueTask<TargetingContext> GetContextAsync()
        {
            HttpContext httpContext = _httpContextAccessor.HttpContext;

            if (ACachedTargetingContextExists())
            {
                return CachedTargetingContext();
            }

            ClaimsPrincipal user = httpContext.User;
            TargetingContext targetingContext = new TargetingContext
            {
                UserId = user.Identity.Name,
                Groups = GetGroupsFromClaims()
            };

            CacheTargetingContextForFutureLookups();

            return new ValueTask<TargetingContext>(targetingContext);

            // Local functions could be moved to class level functions
            bool ACachedTargetingContextExists() => httpContext.Items.ContainsKey(CacheKey);
           
            ValueTask<TargetingContext> CachedTargetingContext() =>  new ValueTask<TargetingContext>((TargetingContext)httpContext.Items[CacheKey]);
           
            IEnumerable<string> GetGroupsFromClaims()
            {                               
                // In this implementation groups/roles are specified using claims (ClaimTypes.Role)
                foreach (Claim claim in user.Claims)
                {
                    if (claim.Type == ClaimTypes.Role) 
                    {
                        yield return claim.Value;
                    }
                }
            }
          
            void CacheTargetingContextForFutureLookups() => httpContext.Items[CacheKey] = targetingContext;
        }
    }
}

In the preceding code, we are basically getting the user id of the current request and also any roles/groups to which that user belongs. This is in the form of a TargetingContext instance. This TargetingContext instance will be used by the targeting feature filter to decide whether or not to give the user access to the feature.

Configuring Targeting

To configure the targeting filter, you specify the users and/or groups, and the percentage of all users:

"FeatureManagement": {
  "Printing": {
    "EnabledFor": [
      {
        "Name": "Microsoft.Targeting",
        "Parameters": {
          "Audience": {
            "Users": [
              "Sarah",
              "Amrit"
            ],
            "Groups": [
              {
                "Name": "earlyadopters",
                "RolloutPercentage": 10
              }
            ],
            "DefaultRolloutPercentage": 0
          }
        }
      }
    ]
  }
}

In the preceding config, the Printing feature will be enabled for:

  • Sarah and Amrit
  • 10% of all users in the earlyadopters role/group
  • 0% of all users

Once the feature has been running in production without error this could be expanded to all the early adopters:

"FeatureManagement": {
  "Printing": {
    "EnabledFor": [
      {
        "Name": "Microsoft.Targeting",
        "Parameters": {
          "Audience": {
            "Users": [
              "Sarah",
              "Amrit"
            ],
            "Groups": [
              {
                "Name": "earlyadopters",
                "RolloutPercentage": 100
              }
            ],
            "DefaultRolloutPercentage": 0
          }
        }
      }
    ]
  }
}

And then at a later point, 50% of non early adopters (i.e. 50% of the user base):

"FeatureManagement": {
  "Printing": {
    "EnabledFor": [
      {
        "Name": "Microsoft.Targeting",
        "Parameters": {
          "Audience": {
            "Users": [
              "Sarah",
              "Amrit"
            ],
            "Groups": [
              {
                "Name": "earlyadopters",
                "RolloutPercentage": 100
              }
            ],
            "DefaultRolloutPercentage": 50
          }
        }
      }
    ]
  }
}

And then to everyone:

"FeatureManagement": {
  "Printing": {
    "EnabledFor": [
      {
        "Name": "Microsoft.Targeting",
        "Parameters": {
          "Audience": {
            "Users": [
              "Sarah",
              "Amrit"
            ],
            "Groups": [
              {
                "Name": "earlyadopters",
                "RolloutPercentage": 100
              }
            ],
            "DefaultRolloutPercentage": 100
          }
        }
      }
    ]
  }
}

Now once the feature has been used for enough time and is considered stable, in a future release the Printing feature flag and associated code can be removed from the app.

SHARE:

Add comment

Loading