C#, .Net and Azure

Settings and feature flag management with Azure App configuration

2019/10/12

Azure App Configuration is the latest Azure offering in regards to configuration.

It is currently in preview and thus offered for free (the GA release is targeted for october so expect pricing soon).

As a central configuration service it offers both feature flags as well as key/value pair style configuration values with tag and label support.

There are already many existing options for configuration in Azure:

App Configuration now offers a central place to manage all these variables and allows updating them at runtime without needing to restart or redeploy the app service.

App Configuration offers many integrations with existing components:

Getting started with Application settings

For app settings it offers a fully flexible key/value system with tagging and labeling support.

The keys do not have to conform to any format, but the recommendation is to use the same Key:Subkey pattern which is also used for all other .Net Core configuration providers.

Custom dimensions

If you have custom dimensions (e.g. customer/region specific overrides) you have multiple options:

If you do use labels, note that you must use the REST api the configuration service offers as the .Net Core config provider has no support for labels.

Setup in .Net Core

Using the Microsoft.Azure.AppConfiguration.AspNetCore nuget package it is quite simple to hook the service into the .Net Core configuration pipeline:

var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder
    .ConfigureAppConfiguration((context, builder) =>
    {
        var config = builder.Build();
        // recommended: use MSI as per https://docs.microsoft.com/azure/azure-app-configuration/howto-integrate-azure-managed-service-identity
        builder.AddAzureAppConfiguration(o => o.ConnectWithManagedIdentity(config["AppConfig:Endpoint"]));
        // alternatively, use a connection string:
        // builder.AddAzureAppConfiguration(config["ConnectionStrings:AppConfig"]);
    })
    .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());

The snippet first creates an IConfiguration instance from the builder and then uses MSI to connect to the app configuration service.

The builder defaults to loading configuration settings from these sources:

When using MSI all that is needed is the config service endpoint url. Which can be stored in the appsetting or an environment variable.

If you don't have experience with MSI, check out my older post.

If you do not want to (or can't) use MSI, then it is also possible to use a connectionstring (as per the comments in the code above).

In that case you will have to set an environment variable in Azure to the connection string and use the Secrets Manager for local development (otherwise you'll end up checking in a secret which is bad practice).

With the setup done, the configuration service is integrated and ready to be used.

Usage

Just like any other setting source, values can be accessed via the IConfiguration interface.

var message = Configuration["Sample:Message"];

The above code assumes that a key Sample:Message exists in the connected app configuration service.

If none is found, then the other sources are probed and if none contains a matching key, null is returned.

Getting started with Feature flags

Feature flags offer great flexibility for a number of scenarios such as:

All of these scenarios benefit from the ability to quickly change flags without needing to redeploy or restart the app.

App Configuration service offers just that as well as flexible definition of feature flags.

Inside the app configuration service it is possible to modify feature flag state based on:

Custom filters allow you to specify and implement your own condition (e.g. "only users who signed up for the beta").

The state evaluation can also combine multiple conditions in which case they are evaluated in order and the first condition to be true causes the flag to be enabled. (Only if all of the conditions are false will the flag be disabled).

An example of combined filters would be:

In this case only if the timeframe is matched and the user is logged in will a message be displayed (and depending on the user group it would look different based on the A/B test).

Setup in .Net Core

Once integrated into your app, the Microsoft.FeatureManagement.AspNetCore package will cache the feature flags in-memory and renew them every 30 seconds by polling the configuration service endpoint (timespan can be configured).

Note the additional UseFeatureFlags line in the startup compared to just using app settings:

var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder
    .ConfigureAppConfiguration((context, builder) =>
    {
        var config = builder.Build();
        // recommended: use MSI as per https://docs.microsoft.com/azure/azure-app-configuration/howto-integrate-azure-managed-service-identity
        builder.AddAzureAppConfiguration(configure =>
            {
                configure
                    .ConnectWithManagedIdentity(config["AppConfig:Endpoint"])
                    .UseFeatureFlags();
            });
    })
    .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>());

and in the startup:

public void ConfigureServices(IServiceCollection services)
{
    services.AddFeatureManagement();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseAzureAppConfiguration();
}

This allows you to write code that depends on these flags without having to worry about refreshing the feature flags yourself (the library guarantees that feature flags won't change within a request via the IFeatureManagerSnapshot interface and also automatically refreshes them by default every 30 seconds).

The guidance on storing feature flags in code is to use enums (but string constants work just as well).

FeatureGateAttribute natively supports string and enum but the IFeatureManager methods expect string, so the calls with an enum would then look like this:

public enum FeatureFlags
{
    Beta,
    NewSignInExperience,
    ..
}

if (_featureManager.IsEnabled(nameof(FeatureFlags.Beta)))

With the feature flags integrated you can then:

Limit access to controllers/actions with FeatureGates

[FeatureGate(FeatureFlags.BetaUsers)]
public class HomeController : Controller
{
    public IActionResult Get()
    {
        ...
    }
}

and

[FeatureGate(FeatureFlags.BetaUsers)]
public IActionResult Get()
{
    ...
}

These endpoints will return 404 automatically when the feature flag is turned off (can be customized via services.AddFeatureManagement().UseDisabledFeaturesHandler() in the startup).

Change the routing/middlewares depending on feature flags

app.UseMiddlewareForFeature<BetaMiddleware>(nameof(FeatureFlags.Beta));
app.UseForFeature(featureName, appBuilder => {
    appBuilder.UseMiddleware<T>();
});

Change content of views

<feature name="Beta">
    <p>This can only be seen if 'Beta' is enabled.</p>
</feature>

Modify code flow

private readonly IFeatureManager _featureManager;
    
private void DisplayGreeting()
{
    if (_featureManager.IsEnabled(nameof(FeatureFlags.Beta)))
    {
        ...
    }
}

For more setup details, see the app configuration documentation.

Custom filters

The final topic I want to touch on are custom filters.

Using the nuget package Microsoft.FeatureManagement.AspNetCore it is possible to define custom filters.

Such custom filters can be used to enable feature flags on any conditions based on e.g.

Below is an example of a filter that checks the users claims. It will only evaluate to true if the user has the custom claim channel.beta set.

Because the filter hooks into the standard Asp.Net pipeline we can use claims from the IHttpContextAccessor (which in turn can pull claims from the access token or from a custom claims transform which might use a database, ...).

The endresult: Only users with the specific claim will get access to endpoints tagged with [FeatureGate("BetaUsers")].

Here's the filter:

[FilterAlias("BetaUsers")]
public class BetaUsersFilter : IFeatureFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public BetaUsersFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public bool Evaluate(FeatureFilterEvaluationContext context)
    {
        return _httpContextAccessor.HttpContext.User.HasClaim(ClaimTypes.Role, "channel.beta");
    }
}

tagged as .Net Core, Azure, Feature flags and Configuration