This is part eight in a series of articles.
EDIT: my Feature Management Pluralsight training course is now available.
In part six of this series, we saw how to prevent a feature flag from changing during processing of a single request.
In this article we’re going to look at how to maintain consistency across multiple requests for the same user/session.
The Problem
Consider the following scenario that introduces a feature flag MustCaptureAge. For example suppose new legal regulations require that from a certain date or time that the application must now capture the age of a customer for all purchases:
- New age functionality is deployed to production with MustCaptureAge flag set to false
- User 42 navigates to web app
- User 42 adds an item to their cart
- User 42 navigates to checkout
- At this point MustCaptureAge=false so no Age input is displayed
- User 42 starts typing information onto checkout page
- Whilst User 42 is typing, MustCaptureAge flags gets set to true
- User 42 clicks submit
- What happens now? Age was not captured…
In the preceding scenario the MustCaptureAge feature flag changed between requests. At the start it was set to false but at some point during the user’s interaction it got set to true. This type of scenario could cause unpredictable results, legal breaches, data corruption, missed sales, annoyed customers, etc.
One way to fix this is to maintain the feature flag value across all requests for a given session, essentially taking the initial value of the feature flag and caching it for all subsequent requests in that session.
Preserving Feature Flag Values Across Multiple ASP.NET Core Requests
The ISessionManager interface in the Microsoft.FeatureManagement namespace allows the implementer to store the state of feature flags for a session. This interface is not specific to ASP.NET and can be used in non web-applications. It also has no dependency on HTTP context. We can however implement a version that can be used in an ASP.NET Core web app.
The interface has 2 methods to get and set the value of a feature flag. Inside these methods we can make use of the current HTTP session context to store the feature flag value. Essentially the first time the feature flag is looked up for a session, its value will be persisted in session state for the remainder of the session, even across multiple pages/requests.
As an example, the following code shows a basic implementation that uses an IHttpContextAccessor to get access to the HttpContext of the current request and then store or retrieve the value in ASP.NET session state:
using Microsoft.AspNetCore.Http;
using Microsoft.FeatureManagement;
using System;
using System.Threading.Tasks;
namespace WebApplication1.Models
{
/// <summary>
/// Based on https://andrewlock.net/keeping-consistent-feature-flags-across-requests-adding-feature-flags-to-an-asp-net-core-app-part-5/
/// </summary>
public class HttpContextFeatureSessionManager : ISessionManager
{
private readonly IHttpContextAccessor _contextAccessor;
private const string SessionKeyPrefix = "feature_flag_";
public HttpContextFeatureSessionManager(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public Task<bool?> GetAsync(string featureName)
{
bool keyExistsInHttpSession = _contextAccessor.HttpContext
.Session
.TryGetValue(key: $"{SessionKeyPrefix}{featureName}",
value: out byte[] bytes);
if (keyExistsInHttpSession)
{
return Task.FromResult((bool?)BitConverter.ToBoolean(bytes));
}
return Task.FromResult<bool?>(null);
}
public Task SetAsync(string featureName, bool enabled)
{
_contextAccessor.HttpContext
.Session
.Set(key: $"{SessionKeyPrefix}{featureName}",
value: BitConverter.GetBytes(enabled));
return Task.CompletedTask;
}
}
}
To plug the above HttpContextFeatureSessionManager into the ASP.NET Core app, modify the Startup.ConfigureServices method and add services.AddTransient<ISessionManager, HttpContextFeatureSessionManager>(); and to enable session state: services.AddSession(); You’ll also need to add app.UseSession(); in the Configure method:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.FeatureManagement;
using WebApplication1.Models;
namespace WebApplication1
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
services.AddTransient<ISessionManager, HttpContextFeatureSessionManager>();
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddFeatureManagement();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
In the appsettings.json, we can configure the MustCaptureAge feature:
"FeatureManagement": {
"MustCaptureAge": true
}
And then for example use it in the UI:
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<feature name="MustCaptureAge">
<label for="age">Please enter your age</label>
<input name="age" type="number"/>
</feature>
</div>
Now if we run the app, the page will load and show the age input because MustCaptureAge is set to true. However, if we now modify the appsettings.json and change MustCaptureAge to “false” while the web app is still running, and save the file, the existing session that’s open in the browser will still show the age input. If however we open another session (e.g. in another browser) the age input will not be shown.
There are however a number of problems with the HttpContextFeatureSessionManager implementation. We might want only some features to be preserved across requests but other features to be updated immediately regardless of if a session is currently underway.
One way to opt in, if using strongly typed feature names via an enum (see part two) is to define a custom attribute:
using System;
namespace WebApplication1.Models
{
public sealed class PreserveFeatureAttribute : Attribute { }
}
And then decorate any enum value feature flags that you want to preserve across requests:
public enum Features
{
Printing,
[PreserveFeature]
MustCaptureAge
}
In the preceding enum, the Printing feature will not be preserved for a session, but the MustCaptureAge feature will be consistent for requests in a single session.
To make use of this attribute, the HttpContextFeatureSessionManager can be modified. Using some reflection code (which may not be the fastest as it will be performed on every request) we can examine whether or not the enum value has the attribute and only set the session item if it does:
using Microsoft.AspNetCore.Http;
using Microsoft.FeatureManagement;
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace WebApplication1.Models
{
/// <summary>
/// Based on https://andrewlock.net/keeping-consistent-feature-flags-across-requests-adding-feature-flags-to-an-asp-net-core-app-part-5/
/// </summary>
public class HttpContextFeatureSessionManager : ISessionManager
{
private readonly IHttpContextAccessor _contextAccessor;
private const string SessionKeyPrefix = "feature_flag_";
public HttpContextFeatureSessionManager(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public Task<bool?> GetAsync(string featureName)
{
bool keyExistsInHttpSession = _contextAccessor.HttpContext
.Session
.TryGetValue(key: $"{SessionKeyPrefix}{featureName}",
value: out byte[] bytes);
if (keyExistsInHttpSession)
{
return Task.FromResult((bool?)BitConverter.ToBoolean(bytes));
}
return Task.FromResult<bool?>(null);
}
public Task SetAsync(string featureName, bool enabled)
{
if (!ShouldPreserveAccrossRequests(featureName))
{
return Task.CompletedTask;
}
_contextAccessor.HttpContext
.Session
.Set(key: $"{SessionKeyPrefix}{featureName}",
value: BitConverter.GetBytes(enabled));
return Task.CompletedTask;
}
private static bool ShouldPreserveAccrossRequests(string featureName)
{
MemberInfo enumFieldInfo = typeof(Features).GetMember(featureName).First();
if (enumFieldInfo.GetCustomAttributes(typeof(PreserveFeatureAttribute), false).Any())
{
return true;
}
return false;
}
}
}
Now if we have the following in the view:
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<feature name="MustCaptureAge">
<label for="age">Please enter your age</label>
<input name="age" type="number" />
</feature>
<feature name="Printing">
<button>Print</button>
</feature>
</div>
The print button can change for different requests in a single session, whereas the age input will never change within a single session.
This reflection based approach also means that the configuring is hard coded into the enum. You could however read session preservation settings from configuration.
Another disadvantage of this approach is when you are using feature filters. A simple true/false feature flag is less complex than a percentage filter or a date range based feature. For example what if a feature is set to turn on automatically by using the Time Window Feature Filter, the feature could automatically turn on during a session, should the current date/time be honoured or should the date/time from the first initial session request be honoured? This is as much a business consideration as it is a technical one and there is no one right answer.
Be sure to check out my Microsoft Feature Management Pluralsight course get started watching with a free trial.
SHARE: