Search code examples
azure-functionsazure-webjobs

Azure function : Updating the config value throws exception (A configuration source is not registered. Please register one before setting a value.)


I am trying to update the value of one of the config values in Configure(IWebJobsBuilder builder) method, it works fine when running locally but when deployed in azure function throws exception.

Exception:

Message:  A configuration source is not registered. Please register one before setting a value.

stack trace :

System.InvalidOperationException:
   at Microsoft.Extensions.Configuration.ConfigurationRoot.set_Item (Microsoft.Extensions.Configuration, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.Extensions.Configuration.ChainedConfigurationProvider.Set (Microsoft.Extensions.Configuration, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.Extensions.Configuration.ConfigurationRoot.set_Item (Microsoft.Extensions.Configuration, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.Extensions.Configuration.ChainedConfigurationProvider.Set (Microsoft.Extensions.Configuration, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.Extensions.Configuration.ConfigurationRoot.set_Item (Microsoft.Extensions.Configuration, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at ConvAi.BfChannel.BotManagementService.WebJobsExtensionStartup.Configure (ConvAi.BfChannel.BotManagementService, Version=0.0.0.0, Culture=neutral, PublicKeyToken=nullConvAi.BfChannel.BotManagementService, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null: E:\Git\skyman - Copy\conversational-ai\src\ConvAi.BfChannel.BotManagementService\WebJobsExtensionStartup.csConvAi.BfChannel.BotManagementService, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null: 99)

I have logged the config providers in app insights and I can see that there are 3 config providers registered.

1.  Microsoft.Extensions.Configuration.ChainedConfigurationProvider
2.  Microsoft.Extensions.Configuration.AzureKeyVault.AzureKeyVaultConfigurationProvider
3.  Microsoft.Extensions.Configuration.Memory.MemoryConfigurationProvider

Here is problematic code:

config["BotManagementServiceBusConnectionString"] =
                    config[serviceBusConnectionStringKey].Remove(
                        config[serviceBusConnectionStringKey].Length - ";EntityPath=Topic1".Length);

Here is the full code:

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="WebJobsExtensionStartup.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
//  --------------------------------------------------------------------------------------------------------------------

using System;
using System.Linq;
using Intercom.Helpers;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

[assembly: WebJobsStartup(typeof(ConvAi.BfChannel.BotManagementService.WebJobsExtensionStartup), "Web Jobs Extension")]
namespace ConvAi.BfChannel.BotManagementService
{
    /// <summary>
    /// WebJobsExtensionStartup
    /// </summary>
    public class WebJobsExtensionStartup : IWebJobsStartup
    {
        /// <summary>
        /// AzureServiceTokenProvider which is used for requesting identity token.
        /// </summary>
        public static AzureServiceTokenProvider AzureServiceTokenProvider { get; set; }

        /// <summary>
        /// Configure services.
        /// </summary>
        /// <param name="builder">WebJob Builder</param>
        public void Configure(IWebJobsBuilder builder)
        {
            try
            {
                bool isLocal = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"));

                // Gets the default configuration
                var serviceConfig = builder.Services.FirstOrDefault(s => s.ServiceType.Equals(typeof(IConfiguration)));
                var rootConfig = (IConfiguration)serviceConfig.ImplementationInstance;
                var keyVault = rootConfig["BFSpeechKeyVault"];
                var serviceBusConnectionStringKey = rootConfig["BotManagementServiceBusConnectionStringKey"];

                if (isLocal)
                {
                    // Use developers's idenity.
                    AzureServiceTokenProvider = new AzureServiceTokenProvider();
                }
                else
                {
                    // Use azure function's managed idenity.
                    var msiClientId = rootConfig["MSI_ClientId"];
                    AzureServiceTokenProvider = new AzureServiceTokenProvider(connectionString: $"RunAs=App;AppId={msiClientId}");
                }

                var keyVaultClient = new KeyVaultClient(
                    new KeyVaultClient.AuthenticationCallback(
                        AzureServiceTokenProvider.KeyVaultTokenCallback));

                var config = new ConfigurationBuilder()
                    .AddConfiguration(rootConfig).AddAzureKeyVault(
                    keyVault,
                    keyVaultClient,
                    new DefaultKeyVaultSecretManager())
                    .AddInMemoryCollection()
                    .Build();

                config["BotManagementServiceBusConnectionString"] =
                    config[serviceBusConnectionStringKey].Remove(
                        config[serviceBusConnectionStringKey].Length - ";EntityPath=Topic1".Length);

                // Replace the existing config
                builder.Services.AddSingleton<IConfiguration>(config);
            }
            catch (Exception ex)
            {
                AppInsights.TrackException(
                    ex,
                    "Failed to start Bot management service",
                    "Sender".PairWith(this.GetType().FullName));
            }
        }
    }
}

Can someone please help me in figuring out what is going on?


Solution

  • I was able to fix the issue by adding the values to the in memory provider.

    Config[""] = value statement tries to add/update the config to every config provider and I could see that ChainedConfigProvider have few providers in the chain which are not registered if they are not being used in the code. ChainedConfigProvider has different set of providers when using locally vs when used in Azure. Due to which statement fails with exception in azure.

    Here is the final code:

    bool isLocal = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WEBSITE_INSTANCE_ID"));
    
    // Gets the default configuration
    var serviceConfig = builder.Services.FirstOrDefault(s => s.ServiceType.Equals(typeof(IConfiguration)));
    var rootConfig = (IConfiguration)serviceConfig.ImplementationInstance;
    var keyVault = rootConfig["BFSpeechKeyVault"];
    var serviceBusConnectionStringKey = rootConfig["BotManagementServiceBusConnectionStringKey"];
    
    if (isLocal)
    {
        TelemetryConfiguration.Active.DisableTelemetry = true;
    
        // Use developers's idenity.
        AzureServiceTokenProvider = new AzureServiceTokenProvider();
    }
    else
    {
        // Use azure function's managed idenity.
        var msiClientId = rootConfig["MSI_ClientId"];
    
        AzureServiceTokenProvider = new AzureServiceTokenProvider(connectionString: $"RunAs=App;AppId={msiClientId}");
    }
    
    var keyVaultClient = new KeyVaultClient(
        new KeyVaultClient.AuthenticationCallback(
            AzureServiceTokenProvider.KeyVaultTokenCallback));
    
    var keyVaultConfig = new ConfigurationBuilder()
        .AddAzureKeyVault(
        keyVault,
        keyVaultClient,
        new DefaultKeyVaultSecretManager())
        .Build();
    
    var botManagementServiceBusConnectionString = Regex.Replace(keyVaultConfig[serviceBusConnectionStringKey], @";EntityPath=Topic1$", string.Empty, RegexOptions.IgnoreCase);
    
    var config = new ConfigurationBuilder()
        .AddConfiguration(rootConfig)
        .AddInMemoryCollection(
            new Dictionary<string, string>
            {
                {
                    "BotManagementServiceBusConnectionString",
                    botManagementServiceBusConnectionString
                }
            })
        .Build();