0

I'm facing issues in reading Nested configuration values from local.settings.json in my Azure Function(v6) and the values always seem to be coming as null. Below are my code snippets:

local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "EndpointDetails": {
      "GetAllTaskDetails": {
        "Url": "https://localhost:7000/api/Task/GetAllTaskDetails",
        "Method": "GET",
        "NotificationRecipients": "user1@domain.com"
      },
      "GetTaskDetails": {
        "Url": "https://localhost:7000/api/Task/GetTaskDetails",
        "Method": "GET",
        "NotificationRecipients": "user2@domain.com"
      }
    }
  }
}

Startup.cs:

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(HealthChecker.Startup))]
namespace HealthChecker
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // Getting the IConfiguration instance
            var configuration = builder.GetContext().Configuration;

            // Registering health checks
            builder.Services.AddHealthChecks();

            // Calling the method that defines health checks
            HealthCheckDefinitions.ConfigureHealthChecks(builder.Services.AddHealthChecks(), configuration);
        }
    }
}

HealthCheckDefinitions.cs:

public static class HealthCheckDefinitions
{
 private static Dictionary<string, List<string>> endpointDetails = new Dictionary<string, List<string>>();

 public static void ConfigureHealthChecks(IHealthChecksBuilder builder, IConfiguration configuration){
     private static IConfigurationSection endpointDetailsListConfig;
     private static string getAllTaskDetails;
     private static string getTaskDetails;
     List<string> endpoints = new List<string>();

     InitializeConfigValues(configuration);

     endpoints.AddRange(new List<string>
            {
                 getAllTaskDetails,
                 getTaskDetails
            });

     foreach (var endpoint in endpoints)
            {
                var endpointDetailsConfig 
                  = endpointDetailsListConfig.GetSection(endpoint);
                GetEndpointDetails(endpointDetailsConfig, endpoint);
               
                foreach (var endpointDetail in endpointDetails)
                {
                  builder.AddCheck(endpointDetail.Key, () =>
                  {
                    // lines of code
                    // .....
                  });
                }
            }
   }
 
 private static void InitializeConfigValues(IConfiguration configuration)
    {
        endpointDetailsListConfig = configuration.GetSection("EndpointDetails");
        getAllTaskDetails = endpointDetailsListConfig.GetSection("GetAllTaskDetails").Key;
        getTaskDetails = endpointDetailsListConfig.GetSection("GetTaskDetails").Key;
    }

   private static void GetEndpointDetails(IConfigurationSection endpointDetailsConfig, string endpoint)
    {
        if (!string.IsNullOrEmpty(endpointDetailsConfig["Url"]) && !string.IsNullOrEmpty(endpointDetailsConfig["Method"])
            && !string.IsNullOrEmpty(endpointDetailsConfig["NotificationRecipients"]))
            {
                endpointDetails.Add(endpoint, new List<string>
                {
                    endpointDetailsConfig["Url"],
                    endpointDetailsConfig["Method"],
                    endpointDetailsConfig["NotificationRecipients"]
                });
            }
    }
}

The values of the following are always null. Where am I going wrong here?

                endpointDetailsConfig["Url"],
                endpointDetailsConfig["Method"],
                endpointDetailsConfig["NotificationRecipients"]
The Inquisitive Coder
  • 1,085
  • 3
  • 20
  • 43
  • 1
    I would expect that you need to traverse the 'config tree' from the very top: `"Values:EndpointDetails"`. As per the [documentation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.iconfiguration.getsection?view=dotnet-plat-ext-7.0#remarks), `IConfiguration.GetSection()` will never return `null`, but rather an empty `IConfigurationSection`; so I am guessing the method call never finds the section `"EndpointDetails"`, but it also doesn't complain about not finding it. – Astrid E. Jul 07 '23 at 14:22
  • 1
    https://stackoverflow.com/users/17213526/astrid-e yes you are correct, it never finds the section "EndpointDetails" and hence the issue. However, one more thing needs to be done, the nested configuration needs to be moved to another config file as otherwise, if we define them in local.settings.json it would create other issues. I have answered my question, please check. Thanks! – The Inquisitive Coder Jul 08 '23 at 15:53
  • 1
    Interesting! I never knew about such config nesting problems. You should mark your own answer as accepted, I am sure other people will be facing this problem in the future as well. – Astrid E. Jul 09 '23 at 05:53

2 Answers2

1

In your local.settings.json file, "GetAllTaskDetails" and "GetTaskDetails" exist here:

{
  "Values": {
    "EndpointDetails": {
      "GetAllTaskDetails": {
        ...
      },
      "GetTaskDetails": {
        ...
      }
    }
  }
}

I.e. at these paths:

"Values:EndpointDetails:GetAllTaskDetails"
"Values:EndpointDetails:GetTaskDetails"

By setting

endpointDetailsListConfig = configuration.GetSection("EndpointDetails");

, you try to target a section that is not at the config root level. Hence, the return value of GetSection() should not be an actual configuration section.

As per the .GetSection() documentation:

This method will never return null. If no matching sub-section is found with the specified key, an empty IConfigurationSection will be returned.

Side note:
To verify whether a section (or value) exists or not, call .Exists() on .GetSection()'s return value.


If you rather target "Values:EndpointDetails", you should be able to traverse the tree correctly from there:

endpointDetailsListConfig = configuration.GetSection("Values:EndpointDetails");
Astrid E.
  • 2,280
  • 2
  • 6
  • 17
1

So, after spending a day breaking my head on this issue, I finally came to the solution thanks to this post which was an eye-opener for me.

Basically you should not add nested configuration in your local.settings.json file, it creates all sorts of other issues(see below issue) when you do so. Hence, I moved my nested configuration into another file like so:

Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger, rabbitmqtrigger, orchestrationTrigger, activityTrigger, entityTrigger. You can run 'func azure functionapp fetch-app-settings ', specify a connection string in local.settings.json, or use managed identity to authenticate.

Configuration.json:

   {
     "EndpointDetails": {
          "GetAllTaskDetails": {
            "Url": "https://localhost:7000/api/Task/GetAllTaskDetails",
            "Method": "GET",
            "NotificationRecipients": "user1@domain.com"
          },
          "GetTaskDetails": {
            "Url": "https://localhost:7000/api/Task/GetTaskDetails",
            "Method": "GET",
            "NotificationRecipients": "user2@domain.com"
          }
        }
    }

And my Startup.cs class after necessary changes:

using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(HealthChecker.Startup))]
namespace HealthChecker
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            var context = builder.GetContext();

            // Load configuration from local.settings.json
            var localConfig = new ConfigurationBuilder()
                .SetBasePath(context.ApplicationRootPath)
                .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            // Load configuration from config.json
            var config = new ConfigurationBuilder()
                .SetBasePath(context.ApplicationRootPath)
                .AddJsonFile("configuration.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            // Register the configurations
            builder.Services.AddSingleton(localConfig);
            builder.Services.AddSingleton(config);

            // Registering health checks
            builder.Services.AddHealthChecks();

            // Calling the method that defines health checks
            HealthCheckDefinitions.ConfigureHealthChecks(builder.Services.AddHealthChecks(), localConfig, config);
        }
    }
}

And that's pretty much it. You can access these values easily wherever you want to, viz:

HealthCheckDefinitions.cs

public static class HealthCheckDefinitions
    {
     private static Dictionary<string, List<string>> endpointDetails = new Dictionary<string, List<string>>();

     public static void ConfigureHealthChecks(IHealthChecksBuilder builder, IConfiguration configuration){
       private static IConfigurationSection endpointDetailsListConfig;
       private static string getAllTaskDetails;
       private static string getTaskDetails;
       List<string> endpoints = new List<string>();
   
       var x = config["EndpointDetails:GetAllTaskDetails:Url"];
       //lines of code
       //...
 }

 //lines of code
 //...
}

Edit:

One more thing, please make sure that CopyToOutputDirectory is correctly set. Refer this if required. Below is my .csproj file:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="AzureFunctions.Extensions.DependencyInjection" Version="1.1.3" />
    <PackageReference Include="Microsoft.Azure.Functions.Extensions" Version="1.1.0" />
    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.1.1" />
  </ItemGroup>
  <ItemGroup>
    <None Update="configuration.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </None>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

I tried to give all the steps that I followed to fix this issue, hope it helps!

The Inquisitive Coder
  • 1,085
  • 3
  • 20
  • 43