Is IFeatureManager cache not refreshed for the first call after cache has expired?

650 Views Asked by At

In ASP.NET Core 6 minimal API, I've been working with Azure App Configuration feature flags. I have set up the feature flag configuration so that the flags expire in 5 seconds.

builder.Configuration.AddAzureAppConfiguration(
    options => options.UseFeatureFlags(opts => opts.CacheExpirationInterval = TimeSpan.FromSeconds(5)));

I also have added Azure App Configuration and Feature Management services

builder.Services.AddAzureAppConfiguration();
builder.Services.AddFeatureManagement();

And set up the usage

app.UseAzureAppConfiguration();

I tried out one of the feature flags if it is enabled with code below

bool isServiceEnabled = await _featureManager.IsEnabledAsync(FeatureFlags.IsServiceEnabled);

At first it does read the correct value from the App Configuration, then I tried toggling it and calling API after the cache expires, first call to the API still shows me the old value. It's only the second call to the API after expiration that does show the new value.

It seems like first API call still has the old value cached.

Have I missed something? Did I do something wrong while setting up the feature flags?

3

There are 3 best solutions below

0
On

I don't know if this is the proper answer, but what worked for us was that we had to register IConfiguration as a singleton. Yes the framework should have done that for us already but when added this line, any feature flag update would be retrieved. In my startup.cs, I did this.

// Get IConfiguration through DI
private IConfiguration Configuration { get; set;}

public Startup(IConfiguration configuration)
{
   Configuration = configuration;
}

// (if you're not using the minimal API)
public void ConfigureServices(IServiceCollection services)
{
   services.AddSingleton(Configuration);
}
0
On

So what I do is:

  1. I grab the refresher from AzureAppConfigurationOptions
  2. Call the method TryRefreshAsync on an interval using a BackgroundService.

Example:

        builder.Configuration.AddAzureAppConfiguration(options =>
        {
            options.Connect(connectionString)
                .Select("*", AzureAppConfigurationLabel(appConfiguration))
                .ConfigureRefresh(refresh =>
                {
                    refresh.Register("sentinel", AzureAppConfigurationLabel(appConfiguration), refreshAll: true)
                        .SetCacheExpiration(TimeSpan.FromMinutes(1));
                });

            // Load all feature flags with environment label
            options.UseFeatureFlags(featureFlagOptions =>
            {
                featureFlagOptions.Select(KeyFilter.Any, AzureAppConfigurationLabel(appConfiguration));
            });

            refresher = options.GetRefresher();
        });

And then in my BackgroundService

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                var level = GetLogLevel();
                Log.Information( $"Updating Configuration information {DateTime.Now}");
                var success = await _refresher.TryRefreshAsync(stoppingToken);
0
On

This is by design.

The following code adds AzureAppConfigurationRefreshMiddleware to the request pipeline.

    app.UseAzureAppConfiguration();

See https://github.com/Azure/AppConfiguration-DotnetProvider/blob/main/src/Microsoft.Azure.AppConfiguration.AspNetCore/AzureAppConfigurationRefreshExtensions.cs

This middleware does activity-based refresh and has the following code.

    public async Task InvokeAsync(HttpContext context)
    {
        foreach (var refresher in Refreshers)
        {
            _ = refresher.TryRefreshAsync();
        }

        // Call the next delegate/middleware in the pipeline
        await _next(context).ConfigureAwait(false);
    }

Note that there is no await for refresher.TryRefreshAsync();. Thus you still get the dirty data until the refresh succeeds.

See https://github.com/Azure/AppConfiguration-DotnetProvider/blob/main/src/Microsoft.Azure.AppConfiguration.AspNetCore/AzureAppConfigurationRefreshMiddleware.cs