Azure App Configuration. On-Demand Refresh

The Problem

Azure App Configuration’s default middleware triggers refresh checks on every HTTP request. With a 30-second cache:

Goal

Implementation

OnDemandAzureAppConfigurationRefresher
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;
        }
    }
}
Install Package
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
Configure App Configuration
// 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>();
        });

Note: Do NOT register app.UseAzureAppConfiguration() middleware in Startup.Configure.

Register Service
// Startup.ConfigureServices
services.AddScoped<IAzureAppConfigurationRefresher, OnDemandAzureAppConfigurationRefresher>();
Expose Refresh Endpoint
[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();
    }
}

Resources