OpenFeature in .NET

Introduction to OpenFeature in .NET

January 25, 2025

Overview

Feature flagging is a mechanism through which features or changes can be toggled on or off within an application, typically without redeploying the code. This would involve adding a package from a vendor, such as Split.IO, and adopting their provided SDK, or APIs, in your codebase. OpenFeature is a community-driven specification that provides a common, vendor-agnostic, API for feature flagging. This post explores why you would want to feature flag and how you can use OpenFeature in .NET.

Why Feature Flag?

Releasing new features to clients can often be cumbersome. Big bang waterfall-style releases are not ideal for a number of reasons, but for the purposes of this post we will on one: lack of customer engagement or feedback. Software changes are not handed to the customer until the very end development cycle, which means a huge amount of time has been invested before any user can get their hands on the new feature.

Releasing changes, bug fixes, and new features more iteratively (say over sprints) mitigates this by giving opportunities to the project stakeholders to get involved early on. However, shipping changes are potentially still all or nothing. Breaking work down into sprints means the MVP (minimum viable product) gets built more iteratively but it does not necessarily mean the changes are shipped incrementally. Shipping frequently, or achieving continuous delivery, means improved code quality - as issues are identified and rectified early - and reduced risk1.

This is where feature flags come in. The development team can ship changes every day while the feature remains hidden and out of sight. Only once the flag is toggled will the new feature show itself. Feature flags can manifest themselves as a simple configuration that is loaded during a build or deployment - maybe through an environment variable. They can also be sourced remotely, using a vendor like Split.IO or PostHog, to enable a more dynamic approach to feature management.

Sometimes you just want to gather some data on why a button should be blue instead of grey, or that searching over a list of items is easier than paginating through the entire collection. Feature flags allow you to finely tune which groups of users see the control, and which see the variable.

SubmitSubmitGroup: ControlGroup: Experiment

In both uses of Feature Flags, they can provide valuable insights to the Product team. This leads to more data-driven decisions being made and greater confidence in the software that is being shipped. This is good for the development team, as time is not wasted on fruitless projects. It is also good for the project stakeholders, as customers continue to see more value in the software they are using.

Code Set up

The following assumes you have the .NET 9 (or greater) SDK installed and a basic understanding of C#.

To get started you can run the following in a shell to generate a basic webapi.

dotnet new webapi --output openfeaturedemo

We need to also add the OpenFeature .NET SDK to the project with NuGet.

dotnet add package OpenFeature OpenFeature.DependencyInjection OpenFeature.Hosting

This will add the project references to the project file and allow the .NET SDK to resolve references to OpenFeature. Next, we can extend the generated sample and add the necessary bits to make OpenFeature work.

Using your preferred code editor open the Program.cs file. You will find a basic weather forecast Web API that returns 5 randomly generated forecasts. We will modify the generated code and add a more-forecasts feature flag that will enable us to return more weather forecasts.

The code below will set up OpenFeature with dependency injection so that we can resolve the necessary classes and interfaces throughout our program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using OpenFeature;
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Providers.Memory;
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

builder.Services.AddOpenFeature(feature =>
{
    feature.AddHostedFeatureLifecycle()
        .AddInMemoryProvider(_ =>
        {
            var variants = new Dictionary<string, bool> { { "on", false } };
            return new Dictionary<string, Flag>
            {
                { "more-forecasts", new Flag<bool>(variants, "on") }
            };
        });
});

var app = builder.Build();

Now that we have initialised OpenFeature with an InMemoryProvider, we can now use OpenFeature in the /weatherforecast API. Add the following using statement to the Program.cs file so that we can leverage the [FromServices] attribute to resolve the injected OpenFeature client.

1
2
3
4
using Microsoft.AspNetCore.Mvc;
using OpenFeature;
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Providers.Memory;

With the .NET Minimal API we can inject any necessary services and use them within the delegate. Below we inject the IFeatureClient and try to get the more-forecasts feature. We default to false if the feature is not found, which will effectively leave the API in the same state prior to the toggle being added.

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
app.MapGet("/weatherforecast", async ([FromServices] IFeatureClient client) =>
{
    var featureOn = await client.GetBooleanValueAsync("more-forecasts", false);
    var numberOfForecasts = featureOn ? 10 : 5;

    var forecast = Enumerable.Range(1, numberOfForecasts).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

Now that the necessary OpenFeature changes have been added we can test that our API is still functioning. You can use the dotnet run command to start the process.

dotnet run

You can open a browser to the URL displayed in the shell output and send a request to /weatherforecast. You should see 5 weather forecasts. Why is this? When we set up the InMemoryProvider we set the “on” variant for the more-forecasts feature flag to return false. We can modify this so that the variant returns true instead.

13
14
15
16
17
18
19
20
.AddInMemoryProvider(_ =>
{
    var variants = new Dictionary<string, bool> { { "on", true } };
    return new Dictionary<string, Flag>
    {
        { "more-forecasts", new Flag<bool>(variants, "on") }
    };
});

When we rerun our application we will see 10 forecasts instead of 5. We have a working feature flag!

Toggles like this are really easy to work with. However, instead of arbitrarily turning on and off a feature we can extend this and use another type of Flag. Let’s make the configuration of how many forecasts are shown more dynamic. Below we update the feature flag name to be number-forecasts and instead of using a Flag<bool> we use an integer instead. The default variant here will be “10”, although we could pick from any of the three variants.

13
14
15
16
17
18
19
20
.AddInMemoryProvider(_ =>
{
    var variants = new Dictionary<string, int> { { "5", 5 }, { "10", 10 }, { "20", 20 } };
    return new Dictionary<string, Flag>
    {
        { "number-forecasts", new Flag<int>(variants, "10") }
    };
});

Next we can make a small change to the API to use the new integer-based feature flag.

39
40
41
42
43
app.MapGet("/weatherforecast", async ([FromServices] IFeatureClient client) =>
{
    var numberOfForecasts = await client.GetIntegerValueAsync("number-forecasts", 5);

    var forecast = Enumerable.Range(1, numberOfForecasts).Select(index =>

Now we have much more flexibility over how many forecasts are shown, and if we want to add new variants we can do so without changing the API at all. If we replaced the InMemoryProvider with a remote provide, like Flagd, we could configure the feature flag without changing the code at all.

Advanced Feature Flags

Let’s say that we want to be able to configure both the number of forecasts and the summary text for each forecast. We could achieve this with two separate feature flags, one as an integer flag and another as a string. Instead, we can leverage the Value and Structure types to build a single flag that can handle both use cases.

First, we need to add a new using statement to ensure we can reference these complex types.

1
2
3
4
using OpenFeature;
using OpenFeature.DependencyInjection.Providers.Memory;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;

Next, we can tweak the InMemoryProvider to set up a more advanced forecasts flag.

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
feature.AddHostedFeatureLifecycle()
    .AddInMemoryProvider(_ =>
    {
        var groupA = new Dictionary<string, Value>
        {
            { "summaries", new Value([new("Freezing"), new("Bracing"), new("Chilly")]) },
            { "numberOfForecasts", new Value(3) }
        };

        var groupB = new Dictionary<string, Value>
        {
            { "summaries", new Value([new("Hot"), new("Sweltering"), new("Scorching")]) },
            { "numberOfForecasts", new Value(6) }
        };

        var variants = new Dictionary<string, Value>
        {
            { "group-a", new Value(new Structure(groupA)) },
            { "group-b", new Value(new Structure(groupB)) },
        };
        return new Dictionary<string, Flag>
        {
            { "forecasts", new Flag<Value>(variants, "group-a") }
        };
    });

Finally, the API needs to be extended to be able to handle this new Object type of feature flag. We can comment out the outer summaries string array as this is no longer needed.

51
52
53
54
//var summaries = new[]
//{
//    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
//};

Next, we can tweak our client method call and replace GetIntegerValueAsync with GetObjectValueAsync. We need to check if the feature IsStructure so we can extract the necessary bits of feature toggle data. In this example I have omitted any null and safety checks, in Production, you should add some defensive code to prevent unexpected errors from occurring.

56
57
58
59
60
61
62
63
64
65
66
67
68
app.MapGet("/weatherforecast", async ([FromServices] IFeatureClient client) =>
{
    var feature = await client.GetObjectValueAsync("forecasts", new Value());

    string[] summaries = ["Freezing", "Bracing", "Chilly", "Cool",  "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
    int numberOfForecasts = 5;
    if (feature.IsStructure)
    {
        summaries = [.. feature.AsStructure!["summaries"].AsList!.Select(value => value.AsString!)];
        numberOfForecasts = feature.AsStructure!["numberOfForecasts"].AsInteger!.Value;
    }

    var forecast = Enumerable.Range(1, numberOfForecasts).Select(index =>

Group A in this example gets 3 forecasts with colder summaries, while Group B gets 6 forecasts with warmer summaries. At the moment though, which group gets shown what is kind of irrelevant. We do not provide any context about the request or user. So we cannot easily identify which user belongs to which group. To resolve this we will extend the sample once more. We will take a specified X-Group request header, and use this to build up a context to pass to OpenFeature.

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var variants = new Dictionary<string, Value>
{
    { "group-a", new Value(new Structure(groupA)) },
    { "group-b", new Value(new Structure(groupB)) },
};

Func<EvaluationContext, string> contextEvaluator = ctx =>
{
    if (ctx.ContainsKey("group"))
    {
        return ctx.GetValue("group").AsString!;
    }
    return "group-a";
};

return new Dictionary<string, Flag>
{
    { "forecasts", new Flag<Value>(variants, "group-b", contextEvaluator) }
};

You may need to use a tool like Postman or Visual Studio built-in support for .http files to be able to send a request to /weatherforecast with the X-Group header. We now have our two groups being shown different feature states, based on context provided by the request. While this example is relatively simple, you could evolve this to include more context (region, data centre, language, etc…). You could build some middleware that creates an EvaluationContext on the fly and passes it along to the request delegate, reducing any boilerplate you may find yourself adding to your API actions or controllers.


  1. The DevOps Handbook by Gene Kim, Jez Humble, Patrick Debois & John Willis covers Continuous Delivery and its benefits but within the context of DevOps. ↩︎