Skip to main content

Using ConfigCat's Feature Flags in an ASP.NET Core Application

· 9 min read
Laszlo Deak (guest)

In this post, I'll investigate integrating ConfigCat's feature management system with an ASP.NET Core 8 Web API Service and the Options<T> pattern. We'll also leverage the built-in polling mechanism of the ConfigCat client library to refresh feature flags' states during the application's runtime. Let's get started!

Using ConfigCat&#39;s Feature Flags in an ASP.NET Core application cover

Feature Flags with ConfigCat and Asp.Net Core Options

Feature flagging or feature toggling is a technique used to enable or disable application features dynamically. For instance, feature flags allow product owners to switch features on or off during the application's runtime. Specific features can be activated or deactivated for particular environments, users, or regions. This approach facilitates A-B testing, testing with a subset of users, or rolling out features to different countries. It can also help comply with certain regional restrictions.

Throughout my career, I've used different sorts of feature flag solutions. In some cases, this was a conscious decision built upon a well-designed application architecture. In contrast, in other cases it was just an if statement with a key-value pair in the configuration file. However, I haven't yet encountered such a complete service as the one provided by ConfigCat.

I will focus on using feature flags in web services, though they are even more crucial for desktop applications. Managing a few web services is simpler than handling hundreds or thousands of desktop applications, which is common in enterprises.

Implementation Options

Feature flags can be leveraged in many ways. In the past, when computer networks were less ubiquitous, feature flags were typically implemented as compiler directives. This method involved compiling or commenting out certain code paths. The advantage was less branching and smaller code size. However, to toggle a feature, recompiling the source code was necessary, making this solution less dynamic. Users would need to reinstall or upgrade their application to see the toggled feature.

Today, the most common technique is branching by if statements. If a feature flag is enabled, a specific code path is executed. For example, when a button is clicked to start order processing, if a feature flag is enabled, an SMS is sent to the user. This could be expressed as:

// ...
ProcessOrder();
var isSmsFeatureEnabled = client.GetValue("sendSMS", false);
if(isSmsFeatureEnabled)
SendSms();
// ...

Another approach would be to leverage branching by abstractions. This is a larger topic, which I won't cover in this post. It might be worth exploring in a separate article.

ConfigCat and Asp.Net Core and .NET 8

Using ConfigCat's feature flagging service does not restrict us from choosing any implementation technique, although using compiler directives is less likely. In this section, I'll demonstrate how to integrate ConfigCat's configuration with the Options<T> pattern in .NET 8.

It's worth noting that no user-specific feature flag will be used, meaning that this implementation will not consider targeting. In all cases, the 'To all users' value of the feature flag is used.

When using the Options<T> pattern, there is no straightforward API to query user-specific settings. In a web application used by multiple users, there is also no effective way to fetch flags for one or a few users during application startup. Therefore, all feature flags shall be independent of users when registered with Options.

We're not going to dive to deep into creating a .NET application, but here's a quick and easy guide on how to do it. First, let me show you the complete startup code and the service's action, then describe the necessary types I will create for the solution. Here is the Program.cs file:

using Microsoft.Extensions.Options;
using ConfigCatInDotnetSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Add ConfigCat configuration
builder.Configuration.AddConfigCat(builder.Configuration["ConfigCat:Key"],
TimeSpan.Parse(builder.Configuration["ConfigCat:PollInterval"]));

// Configure services to use ConfigCat settings
builder.Services.ConfigureConfigCat(builder.Configuration);

// Add logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();

var app = builder.Build();

app.UseHttpsRedirection();

// Define a route that handles GET requests to /api/feature
app.MapGet("/api/feature", async (HttpContext context, IOptionsSnapshot<FeatureSet> features) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
try
{
var featureValue = features.Value.myFeature;
logger.LogInformation($"myFeature is set to: {featureValue}");
if (featureValue)
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("the feature flag is on");
}
else
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("the feature flag off");
}
}
catch (Exception ex)
{
logger.LogError(ex, "Error while retrieving the feature flag value.");
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsync("Internal Server Error");
}
});

app.Run();

public class FeatureSet
{
public bool myFeature { get; set; }
}

This web API has a single GET endpoint: /api/feature. The response depends on a feature flag: myFeature. When the feature is turned on, it returns HTTP 200 OK with a message indicating the feature flag is on. When the feature is turned off, it returns a message stating otherwise. The state of the feature flag is accessed through IOptionsSnapshot<FeatureSet> features, which I will explain later in this post.

The builder.Configuration.AddConfigCat(); uses a custom extension method to add ConfigCat's toggle values to the ASP.NET Core configuration. The line services.Configure<FeatureSet>(configuration.GetSection("FeatureSet")); sets up the feature flags with the Options<T> pattern. This standard method binds a given section of the configuration to a type and registers the type with the DI container. Here, I'll bind the configuration to a type called FeatureSet with a single boolean property myFeature.

Let's look at the custom extension method. The following code integrates ConfigCat configuration values into ASP.NET's setup. The extension method uses ConfigurationManager to read the ConfigCat API key and poll interval settings from appsettings.json. These values are added by the ASP.NET application's file provider during startup.

In production, it's better to pass the ConfigCat key as a secret or environment variable. The configuration source should set this value before calling AddConfigCat. Since '0' is an invalid polling interval, the method validates it. The method also has an optional parameter to handle cases when ConfigCat can't fetch the feature flags, and an onError parameter to handle exceptions. Feature flags are periodically fetched from the service, and onError provides a way to track errors during these background polls.

// Configuration/ConfigCatExtensions.cs
using ConfigCat.Client;

namespace ConfigCatInDotnetSample.Configuration
{
public static class ConfigCatExtensions
{
public static IConfigurationBuilder AddConfigCat(this IConfigurationBuilder builder, string sdkKey, TimeSpan pollInterval)
{
var configSource = new ConfigCatConfigurationSource(sdkKey, pollInterval);
builder.Add(configSource);
return builder;
}

public static IServiceCollection ConfigureConfigCat(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<FeatureSet>(configuration.GetSection("FeatureSet"));
return services;
}
}

//...
}

You can view the complete code here.

The ConfigCatOptions record type encapsulates the parameters for ConfigCatConfigurationProvider.

The next type is ConfigCatConfigurationSource. An IConfigurationSource is required to be implemented as this is the type added to the configuration sources. The responsibility of the type is to create an IConfigurationProvider. With .NET 8, when a configuration provider is removed or modified, all the remaining sources are rebuilt. This implementation returns a lazily instantiated ConfigCatConfigurationProvider instance. I will use singleton semantics because auto-polling built into the ConfigCatConfigurationProvider refreshes the configuration automatically.

public class ConfigCatConfigurationSource : IConfigurationSource
public class ConfigCatConfigurationSource : IConfigurationSource
{
private readonly string _sdkKey;
private readonly TimeSpan _pollInterval;

public ConfigCatConfigurationSource(string sdkKey, TimeSpan pollInterval)
{
_sdkKey = sdkKey;
_pollInterval = pollInterval;
}

public IConfigurationProvider Build(IConfigurationBuilder builder) =>
new ConfigCatConfigurationProvider(_sdkKey, _pollInterval);
}

Another use case could be when the feature flags are read-only at application startup. In certain applications, this could be a valid scenario. For this, manual polling would be a better choice, and creating a new instance of ConfigCatConfigurationProvider on every Build() method invocation would also make sense.

The last and most complex class to implement is ConfigCatConfigurationProvider. This type derives from ConfigurationProvider which already implements many of the IConfigurationProvider interface members. Here, I'll only override the Load() method, which is invoked by the Host right after the configuration provider is instantiated. In the first invocation, I'll create a new ConfigCatClient.

 public class ConfigCatConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly IConfigCatClient _client;
private readonly Timer _timer;
private readonly ILogger<ConfigCatConfigurationProvider> _logger;

public ConfigCatConfigurationProvider(string sdkKey, TimeSpan pollInterval)
{
var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
});
_logger = loggerFactory.CreateLogger<ConfigCatConfigurationProvider>();

_client = ConfigCatClient.Get(sdkKey, options =>
{
options.PollingMode = PollingModes.ManualPoll;
options.Logger = new ConsoleLogger(ConfigCat.Client.LogLevel.Warning);
});

// Initial data load
LoadAsync().Wait(); // Block until initial load completes
_timer = new Timer(async _ => await RefreshConfigAsync(), null, pollInterval, pollInterval);
}

private async Task RefreshConfigAsync()
{
await LoadAsync();
}

public override void Load()
{
LoadAsync().Wait(); // Block until load completes
}

public async Task LoadAsync()
{
var config = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
await _client.ForceRefreshAsync(); // Ensure we fetch the latest config

var allKeys = await _client.GetAllKeysAsync();
Console.WriteLine($"All keys from ConfigCat: {string.Join(", ", allKeys)}");

var settings = _client.Snapshot().FetchedConfig.Settings;

foreach (var setting in settings)
{
var key = setting.Key;
var value = setting.Value;
Console.WriteLine($"Feature Flag: {key} | Value: {value}");
}
Data = config;
OnReload();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading configuration from ConfigCat.");
}
}

public void Dispose()
{
_timer.Dispose();
_client.Dispose();
}
}

Once ConfigCatClient has loaded the data, the OnConfigurationChanged event is fired. This is when all key-value pairs are loaded. LoadData() and ParseKeys() methods read and parse the keys and corresponding values. The result dictionary is set in the Data property, which is declared by the base class. The only additional logic applied here is to replace the underscore characters with semicolons. This is done because the ':' character is unsupported in key names. To deal with the hierarchy of configuration values, another character must be used for the ConfigCat feature names. Using the \_ character resembles a similar behavior to using configuration values with environment variables.

Note that the LoadData() method invokes a method from the base type: OnReload();. This generates a new change token signaling the configuration provider that the configuration values have changed. The values of options might change due to the built-in auto-polling mechanism; however, the OnConfigurationChanged event is only fired when the values have changed.

To read the latest configuration values while serving the HTTP request, an IOptionsSnapshot<FeatureSet> is passed to the GET request's action handler. This type is useful in scenarios where options should be recomputed on every request.

Sample App

You can find the complete code for the sample application here.

Conclusion

An aging but maintained application needs a feature flag solution. The more robust the solution, the more options the development team has to isolate preview features for specific users. Building a custom feature flag solution usually doesn't offer a competitive advantage, so using a purpose-built service makes sense. ConfigCat's solution seems like a reasonable choice for my next project.

ConfigCat supports simple feature toggles, user segmentation, and A/B testing and has a generous free tier for low-volume use cases or those just starting out.

For additional feature flagging tips and resources, stay connected with ConfigCat on X, Facebook, LinkedIn, and GitHub.