'Implementing Azure Event Grid event handler in a Web API which subscribes to changes in Azure App Configuration

I am exploring various methods to get configuration data from Azure App Configuration and got most methods working however I am struggling to implement the Azure Event Grid event handler in a Web API on ASP.NET Core 3.1. I want to know if it's possible to notify the Web API about changes through the Event Grid instead of it polling when its cache set up for Azure App configuration has expired. Once notified, the configuration values should update and the new values should feed to any controllers when someone makes a request to the Web API.

I have included the contents in my Program.cs, Startup.cs, service bus consumer where I set up the event grid subscriber and a sample controller.

From what I've read from Microsoft's documentation about this, my understanding is that the IConfigurationRefresher.ProcessPushNotification method resets the cache expiration to a short random delay rather the cache expiration set in the CreateHostedBuilder method and when the IConfigurationRefresher.TryRefreshAsync() is called it updates the configuration values.

The issue I am having is not being able to inject an instance of the concrete class for IConfigurationRefresher to the RegisterRefreshEventHandler method and in turn call ProcessPushNotification to reset the expiration time.

I may also be going about this wrongly as I'm assuming the RegisterRefreshEventHandler code which works in a console application will work for a Web API as well. Please let me know if my approach will work?

Program.cs

   public class Program
    {
        private static IConfiguration _configuration;

        private static IConfigurationRefresher _refresher;

        public static void Main(string[] args)
        {

            _configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", optional: false).Build();

            CreateHostBuilder(args).Build().Run();

        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            webBuilder.ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();
                config.AddAzureAppConfiguration(options =>
                    {
                        options
                            .Connect(_configuration["AppConfig"]).ConfigureRefresh(
                            refresh => refresh.Register(key: "Api:Sentinel", refreshAll: true).SetCacheExpiration(TimeSpan.FromDays(1))

                            ).Select(KeyFilter.Any, "ClientA");

                        _refresher = options.GetRefresher();
                    }
                );
            }).UseStartup<Startup>());
    }

Startup.cs

   public class Startup
    {
        public Startup(IConfiguration configuration, IConfigurationRefresher configurationRefresher)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TFASettings>(Configuration.GetSection("Api:TFA"));
            services.AddControllers();
            services.AddAzureAppConfiguration();
            services.AddSingleton<IServiceBusConsumer, ServiceBusConsumer>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            var bus = app.ApplicationServices.GetRequiredService<IServiceBusConsumer>();
            bus.RegisterRefreshEventHandler();

            app.UseAzureAppConfiguration();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

ServiceBusConsumer.cs

    public class ServiceBusConsumer : IServiceBusConsumer
    {

        public IConfigurationRefresher configurationRefresher;

        public ServiceBusConsumer()
        {

        }

        public void RegisterRefreshEventHandler()
        {
            SubscriptionClient serviceBusClient = new SubscriptionClient("*****", "*****", "*****");

            serviceBusClient.RegisterMessageHandler(
                handler: (message, cancellationToken) =>
                {
                    // Build EventGridEvent from notification message
                    EventGridEvent eventGridEvent = EventGridEvent.Parse(BinaryData.FromBytes(message.Body));

                    // Create PushNotification from eventGridEvent
                    eventGridEvent.TryCreatePushNotification(out PushNotification pushNotification);

                    //// Prompt Configuration Refresh based on the PushNotification
                    //configurationRefresher.ProcessPushNotification(pushNotification);

                    return Task.CompletedTask;
                },
                exceptionReceivedHandler: (exceptionargs) =>
                {
                    Console.WriteLine($"{exceptionargs.Exception}");
                    return Task.CompletedTask;
                });
        }
    }

Sample controller

    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private readonly TFASettings _tfaSettings;

        public WeatherForecastController(IOptionsSnapshot<TFASettings> tfaSettings)
        {
            _tfaSettings = tfaSettings.Value;
        }

        [HttpGet]
        public WeatherForecast Get()
        {

            return new WeatherForecast
            {
                AuthenticationText = _tfaSettings.AuthenticationWording
            };
        }
    }




Solution 1:[1]

You can get the concrete instance of IConfigurationRefresher through dependency injection. You can even call RegisterRefreshEventHandler() from the constructor too. Your ServiceBusConsumer.cs can look something like this.

private IConfigurationRefresher _configurationRefresher;

public ServiceBusConsumer(IConfigurationRefresherProvider refresherProvider)
{
    _configurationRefresher = refresherProvider.Refreshers.FirstOrDefault();
    RegisterRefreshEventHandler();
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Zhenlan Wang