Dynamically update ASP.NET config on demand using Azure App Configuration

831 views Asked by At

What I'm trying to do: I'm trying to configure update of ASP.Net Configuration on demand using Azure App Configuration as a source and EventGrid subscription to Key-Value modified event with WebHook endpoint.

What my issue is: When the event reaches the endpoint the code is executed without errors but the configuration is not refreshed after all.

Background Information, and what I have tried: I also tried to use polling approach with sentinel key which works well, but doesn't seem to be an optimal solution taking into account Azure App Configuration quota limit or having to wait when the caching has expired.

My code: Here I'm using minimal API syntax

using System.Text.Json;
using AppConfigurationSpike;
using Azure.Identity;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.SystemEvents;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Options;

var builder = WebApplication
    .CreateBuilder(args);

// Load configuration from Azure App Configuration
builder.Host.ConfigureAppConfiguration((context, config) =>
{
    var settings = config.Build();
    config.AddAzureAppConfiguration(options =>
    {
        options.Connect(new Uri(settings["MyApp:AppConfigurationEndpoint"]), new DefaultAzureCredential())
            // Load all keys that start with `MyApp:` and have no label
            .Select("MyApp:*")
            // Configure to reload configuration if the registered sentinel key is modified
            .ConfigureRefresh(refreshOptions =>
                refreshOptions.Register("MyApp:Settings:Sentinel", refreshAll: true)
                    .SetCacheExpiration(TimeSpan.FromDays(30)));
    });
});

// Add Azure App Configuration middleware to the container of services.
builder.Services.AddAzureAppConfiguration();
builder.Services.Configure<Settings>(builder.Configuration.GetSection("MyApp:Settings"));

var app = builder.Build();

// Use Azure App Configuration middleware for dynamic configuration refresh.
app.UseAzureAppConfiguration();

// I use this endpoint for checking whether the configuration is updated or not.
app.MapGet("/", (IOptionsSnapshot<Settings> settings) => settings.Value.Key);

// A webhook for immediate configuration update.
app.MapPost("/api/update_config", async (HttpContext context, IConfigurationRefresherProvider refresherProvider, ILogger<Program> logger) =>
{
    logger.LogInformation($"Entered update_config...");
    var refresher = refresherProvider.Refreshers.First();
    var data = await BinaryData.FromStreamAsync(context.Request.Body);
    var eventGridEvent = EventGridEvent.Parse(data);
    // Handle system events
    if (eventGridEvent.TryGetSystemEventData(out object eventData))
    {
        // Handle the subscription validation event. This is needed to register this webhook during event subsscription creation.
        if (eventData is SubscriptionValidationEventData subscriptionValidationEventData)
        {
            var responseData = new SubscriptionValidationResponse()
            {
                ValidationResponse = subscriptionValidationEventData.ValidationCode
            };
            await context.Response.WriteAsync(JsonSerializer.Serialize(responseData));
        }

        if (eventData is AppConfigurationKeyValueModifiedEventData)
        {
            logger.LogInformation($"Updating config data...");
            eventGridEvent.TryCreatePushNotification(out PushNotification pushNotification);
            // Invalidate cached config
            refresher.ProcessPushNotification(pushNotification);
            
            // Also tried this, but it doesn't update the config
            // refresher.SetDirty(TimeSpan.FromSeconds(1));
            // await Task.Delay(TimeSpan.FromSeconds(1));
            
            var result = await refresher.TryRefreshAsync();
            if (result)
            {
                logger.LogInformation("Config has been updated");
            }
        }
    }
});

app.Run();

And this is the Settings class:

public class Settings
{
    public string Key { get; set; } = null!;
    public string Sentinel { get; set; } = null!;
}

When I change a key in the Azure App Configuration I see that the event handler has executed and I see the following messages in the application logs:

Entered update_config...
Trying to update config settings...
Result is True

But when I use GET endpoint I see the previous value.

Update 1: What I found that if I add "Key" settings to the refreshOptions.Register method than the the configuration cache is invalidated and I got new key-value from Azure, so I made this change to ConfigureRefresh section:

.ConfigureRefresh(refreshOptions =>
   refreshOptions.Register("MyApp:Settings:Key", refreshAll: true)
   .SetCacheExpiration(TimeSpan.FromDays(30)));

But for me it seems not obvious why I need to register each configuration key in the RefreshOptions?

Update 2: I just tried removing the handler completely and check what will be if I change the key value in the App Configuration. When polling my GET endpoint I noticed the value has changed after 20-30 minutes, however, the exiration duration was set to 30 days. I check that App Service hasn't been restarted during this time. Does it mean this expiration duration is limited internallly?

Follow up questions:

  • Is it possible to specify that I need to monitor for all keys in the configuration?
  • Can I completely disable polling of App Configuration and configuration caching in case I have a webhook for updating the key-values? Currently, I see this will not work if I remove ConfigureRefresh sections.
  • Why I need to explicitly invalidate configuration cache before I call await refresher.TryRefreshAsync();? I expected this invalidation should be inside the TryRefreshAsync method.
1

There are 1 answers

0
Zhenlan Wang On

The push model in App Configuration is designed to serve as a notification for configuration changes. It is NOT designed to deliver the actual configuration. Only the application knows what set of configurations to load and under what conditions the configuration should be reloaded. Therefore, a change notification alone shouldn't trigger a configuration update. This ensures the consistency of the configuration to an application.

The idea is that, first, you tell your application what configuration to load and what configuration to monitor for reloading. This is the same setup as what you would do for a poll model. The only difference is that you set a much longer polling interval. Then, when a change notification comes in, your application will reset the monitoring waiting time and immediately issue a request to App Configuration for the key it is set up to monitor. If the key is changed, it will reload the configuration; otherwise, it will do nothing.

I hope the above explains why you see what you see. In your case, if your application is set up to reload configuration only if the sentinel key is changed, you must make a change to the sentinel key for the configuration to reload. When you make changes to other keys, although they trigger push notifications to your application, from your application's point of view, it's not ready to reload the configuration because the sentinel key is not changed. So the push notifications of other keys will effectively be ignored.

I hope this helps. I strongly suggest you read the discussion in the document section Register event handler to reload data from App Configuration to learn some more details.