Azure App Configuration refresh on demand

Here’s what Azure App Configuration Service is.

Here’s how to use it.

How the web api middleware uses the App Configuration Client

As per oficial documentation, refering to

// Startup.Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseAzureAppConfiguration();

“The middleware uses the refresh configuration specified in the AddAzureAppConfiguration method in Program.cs to trigger a refresh for each request received by the ASP.NET Core web app. For each request, a refresh operation is triggered and the client library checks if the cached value for the registered configuration setting has expired. If it’s expired, it’s refreshed.”

It’s worth to note that the refresh doesn’t happen synchronously, but as fire & forget (you can check so in the code of the middleware), so it actually does not impact the incomming request’s response time. Rather the next request is the one that will see the change in the value.

On demand configuration refresh

But it doesn’t make sense to have one request every 30 seconds to ask for something that changes only once in a long while (not to talk about infinite cahe and having to restart the application when changing it).

Plus it does impact in cost, because considering a cache of 30 sec (the default) and an with one request every 30 seconds, you get one request to the ‘sentinel’ key every 30 seconds to the App Config Service, PER SERVICE with this configurations. So, 10 services make over 200k calls a day, and that is actually the limit above which the service starts charging extra. Not good.

So, this is not the behaviour I want, because:

What I want is:

So, I implement an OnDemandAzureAppConfigurationRefresher and then expose an endpoint to trigger the refresh on demand.

Implementing OnDemandAzureAppConfigurationRefresher

I don’t want the refresh to be triggered by the middleware, instead I want to call it explicitly, so:

public class OnDemandAzureAppConfigurationRefresher : IAzureAppConfigurationRefresher
{
    private readonly List<IConfigurationRefresher> _configurationRefreshers = new List<IConfigurationRefresher>();

    public OnDemandAzureAppConfigurationRefresher(IConfiguration configuration)
    {
        var configurationRoot = configuration as IConfigurationRoot;

        if (configurationRoot == null)
        {
            throw new InvalidOperationException("The 'IConfiguration' injected in OnDemantConfigurationRefresher is not an 'IConfigurationRoot', and needs to be as well.");
        }

        foreach (var provider in configurationRoot.Providers)
        {
            if (provider is IConfigurationRefresher refresher)
            {
                _configurationRefreshers.Add(refresher);
            }
        }
    }

    public async Task RefreshAllRegisteredKeysAsync()
    {
        Task compositeTask = null;
        var refreshersTasks = new List<Task>();
        try
        {
            _configurationRefreshers.ForEach(r => refreshersTasks.Add(r.RefreshAsync()));
            compositeTask = Task.WhenAll(refreshersTasks);
            await compositeTask;
        }
        catch (Exception)
        {
            throw compositeTask.Exception;
        }
    }
}

Add App Configuration configuration

First add the nuget package from the console

dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore

Add the App Configuration as the configuration to use, with a refreshing config (this is a basic config, to focus on the refresh part).

// Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder
                .ConfigureAppConfiguration((hostingContext, config) =>
                    {
                        var settings = config.Build();

                        config
                            .AddAzureAppConfiguration(options => 
                            {
                                options.Connect(settings["ConnectionStrings:AzAppConfigurationConnectionString"])
                                    .ConfigureRefresh(refreshOptions =>
                                    {
                                        refreshOptions.Register("Bustroker.AppConfiguration.WebApi:sentinel", refreshAll: true)
                                                .SetCacheExpiration(TimeSpan.FromSeconds(30));
                                    });
                            });
                    })
                .UseStartup<Startup>();
        });

Register the service

// Startup.ConfigureServices
services.AddScoped<IAzureAppConfigurationRefresher, OnDemandAzureAppConfigurationRefresher>();

Expose an endpoint to trigger App Configuration refresh on demand

Inject it in a Refresh controller to expose the refresh endpoint

// RefreshAzAppConfigurationController

[ApiController]
[Route("[controller]")]
public class RefreshAzAppConfigurationController : ControllerBase
{
    private readonly IAzureAppConfigurationRefresher _azureAppConfigurationRefresher;

    public RefreshAzAppConfigurationController(IAzureAppConfigurationRefresher onDemandConfigurationRefresher)
    {
        _azureAppConfigurationRefresher = onDemandConfigurationRefresher;
    }

    [HttpPost]
    public async Task<IActionResult> Post()
    {
        await _azureAppConfigurationRefresher.RefreshAllRegisteredKeysAsync();
        return Ok();
    }
}

Talk is cheap

Here’s the full code & usage, and here’s the nuget package with the refresher.