With .NET now competing on the cross platform, open source stage, something that often gets overlooked is the number of abstractions that come bundled with the core libraries. One of which I recently started using being Feature Management.
Feature management in .NET is interesting because like others abstractions in the framework, it merely defines the contract by which other code will consume the concept of features. It leaves the implementations however, as something that can be plugged in from multiple sources. I like to think of these designs as conversation starters for projects to rally around. It's especially nice when they allow for multiple implementations to cooperate together!
What You Gain
I think without taking too much away from the official documentation, feature management gives you one central place to see if a feature is available. What those features are and their significance is something that you're empowered to design based on whatever your application needs!
public class MyService
{
private readonly IFeatureManager featureManager;
public MyService(IFeatureManager featureManager)
{
this.featureManager = featureManager;
}
public async Task PerformSomeTask()
{
if (await featureManager.IsEnabledAsync("vnext-preview"))
{
// Do vnext preview things!
}
}
}
It even ships with an ASP.NET filter:
[FeatureGate("vnext-preview")]
public class OrganizationController : Controller
{
// note: You can also apply the annotation above to methods/actions!
}
Putting It To Use
By default, feature management ships with the ability to configure your features statically using the .NET configuration system. This also includes some neat utility filters that can help you do things like A/B testing as well as time based access.
All of this is dependent on you having your features defined at run time, but what if you happen to have some additional feature information that isn't part of your deployments? What if you want to flow-in feature state based on an entitlement or billing system where the data is stored somewhere outside of the application?
Enter feature filters!
I admit, the name still doesn't seem intuitive, even after getting things working. It took me a bit to wrap my head around the specific intent behind the word filter. A feature filter is a piece of code registered as a singleton that gets called numerous times to give a thumbs up or a thumbs down on whether features are enabled.
Upon being asked, feature filters have to be able to source the information they need to return a true
or false
response back to the feature management system.
Keep in mind that bit I mentioned about it being registered as a singleton though, we'll come back to that...
Using Feature Filters During Requests
In my application, I load in data about the current tenant and user on every request and I pack it into a special context object. This data, despite being a complicated graph is easy to cache because it is very consistent between requests. The context object can be thought of something somewhere half way between HttpContext
and DbContext
and is a resource for anywhere in the application where infrastructure and domain must interact.
In this next code sample, you'll see me using it when I use service location to look up a type called AppContext
. Let's get the code in front of us, and then after I'll explain what's going on and also more about the singleton gotcha:
namespace App.Http
{
public class AppFeatureFilter : IFeatureFilter
{
private readonly IHttpContextAccessor httpContextAccessor;
public AppFeatureFilter(
ILogger<AppFeatureFilter> logger,
IHttpContextAccessor httpContextAccessor
)
{
this.logger = logger;
this.httpContextAccessor = httpContextAccessor;
}
public async Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
var httpContext = httpContextAccessor.HttpContext;
// 1.
if (httpContext == null)
{
logger.Log(LogLevel.Information, $"Feature {feature.Name} was checked outside of an http context.");
return false;
}
// 2.
var appContext = httpContext.RequestServices.GetRequiredService<AppContext>();
var currentUser = await appContext.User;
// 3.
return currentUser.Features.Contains(feature);
}
}
}
-
As a bit of due diligence, I like to ensure that if for any reason my filter is called outside of a web request, I don't accidentally crash the application, and I also log a bit of information out. This code is unlikely to be run, but I'll thank myself for logging this here if ever I'm tracing a particularly tricky bug.
-
This is the main reason why I made this blog post as this was by far the most challenging thing to learn about creating a feature filter that is to be used per-request.
You're probably wondering why I couldn't just inject theHttpContext
and be done with things. But remember earlier I mentioned that the feature filter is registered as a singleton? That means that there's only one instance for the entire application and for every web request. Each call made to it is technically done against the same instance. This makes it doubly important to ensure that each time the feature filter is called, it obtains the request for the current thread and answers based on theAppContext
registered in the same scope as thatHttpContext
. -
It probably goes without saying, but in this case, my user object has a property called
Features
which traverses the object graph to build the full list of features available to the user. If you are adapting this example for your own use, this line is where you can figure out what you want to return, atrue
or afalse
!
If you're looking into whether you can make use of the feature management system, or if you've been trying to make sense of it, I hope you've found this quick little summary useful. I think overall, it covers the main details and gotchas that I was missing as I was getting started with .NET feature management.
I think a good supplement to this post would be a quick explainer on the ASP.NET request lifecycle as well as scoped services during dependency injection. There are many resources out there that cover these concepts, although maybe one day I'll put together my own.