blog post image
Andrew Lock avatar

Andrew Lock

~7 min read

Debugging configuration values in ASP.NET Core

Recently I saw this tweet from Cecil Phillip:

This was news to me as well. I have used something similar in many of my applications, but I didn't know there was an extension method to do the hard work for you!

In this post I show how to use GetDebugView() to work out where your configuration values have come from, walk through the source code, and show a simple way to expose the configuration as an endpoint in your ASP.NET Core app. In the next post I'll show another (safer) way to expose this data.

What does IConfigurationRoot.GetDebugView() do?

GetDebugView() is an extension method on IConfigurationRoot that returns a string describing the application configuration. This string displays all of the configuration keys in your application, the associated value, and the source of the value, be it appsettings.json or environment variables for example. A single row looks something like this:

AllowedHosts=* (JsonConfigurationProvider for 'appsettings.json' (Optional))

The key, AllowedHosts comes first, followed by the value *, and the source of the value—in this case the appsettings.json file. Note that this shows the source of the final value in the configuration object.

Note that it's possible that this value may be overwriting a value from a different configuration provider. This view only shows the source of the final value.

Let's look at a bigger example. Let's take a "standard" appsettings.json file from a typical .NET template:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

If you call IConfiguration.GetDebugView() you get a string that looks something like this:

AllowedHosts=* (JsonConfigurationProvider for 'appsettings.json' (Optional))
ALLUSERSPROFILE=C:\ProgramData (EnvironmentVariablesConfigurationProvider)
applicationName=temp (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
ASPNETCORE_ENVIRONMENT=Development (EnvironmentVariablesConfigurationProvider)
ASPNETCORE_URLS=https://localhost:5001;http://localhost:5000 (EnvironmentVariablesConfigurationProvider)
contentRoot=C:\repos\temp (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
DOTNET_ROOT=C:\Program Files\dotnet (EnvironmentVariablesConfigurationProvider)
Logging:
  LogLevel:
    Default=Warning (JsonConfigurationProvider for 'secrets.json' (Optional))
    Microsoft=Warning (JsonConfigurationProvider for 'appsettings.Development.json' (Optional))
    Microsoft.Hosting.Lifetime=Information (JsonConfigurationProvider for 'appsettings.Development.json' (Optional))
MySecretValue=TOPSECRET (JsonConfigurationProvider for 'secrets.json' (Optional))
...

This small example shows configuration values coming from 5 different locations:

  • appsettings.json (JsonConfigurationProvider)
  • appsettings.Developmentjson (JsonConfigurationProvider)
  • secrets.json (JsonConfigurationProvider via the user secrets provider)
  • Environment Variables (EnvironmentVariablesConfigurationProvider)
  • In memory values (ChainedConfigurationProvider)

It's also worth noting how "sections" in the configuration are displayed. For example, the Logging and LogLevel sections from appsettings.json:

Logging:
  LogLevel:
    Default=Warning (JsonConfigurationProvider for 'secrets.json' (Optional))

I was interested to know exactly how this function works, so in the next section we dig into the source code.

Behind the source code of GetDebugView()

The GetDebugView() extension method has been available since .NET Core 3.0. The following shows the code as of .NET 5.0. I show the whole method initially, and then walk through the code afterwards.

using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Microsoft.Extensions.Configuration
{
    public static class ConfigurationRootExtensions
    {
        public static string GetDebugView(this IConfigurationRoot root)
        {
            void RecurseChildren(
                StringBuilder stringBuilder,
                IEnumerable<IConfigurationSection> children,
                string indent)
            {
                foreach (IConfigurationSection child in children)
                {
                    (string Value, IConfigurationProvider Provider) valueAndProvider = GetValueAndProvider(root, child.Path);

                    if (valueAndProvider.Provider != null)
                    {
                        stringBuilder
                            .Append(indent)
                            .Append(child.Key)
                            .Append('=')
                            .Append(valueAndProvider.Value)
                            .Append(" (")
                            .Append(valueAndProvider.Provider)
                            .AppendLine(")");
                    }
                    else
                    {
                        stringBuilder
                            .Append(indent)
                            .Append(child.Key)
                            .AppendLine(":");
                    }

                    RecurseChildren(stringBuilder, child.GetChildren(), indent + "  ");
                }
            }

            var builder = new StringBuilder();

            RecurseChildren(builder, root.GetChildren(), "");

            return builder.ToString();
        }

        private static (string Value, IConfigurationProvider Provider) GetValueAndProvider(
            IConfigurationRoot root,
            string key)
        {
            foreach (IConfigurationProvider provider in root.Providers.Reverse())
            {
                if (provider.TryGet(key, out string value))
                {
                    return (value, provider);
                }
            }

            return (null, null);
        }
    }
}

The GetDebugView() method starts by defining a local function, RecurseChildren() which performs the bulk of the work of the method.

Local functions are private methods that can only be caused by the member they're nested in. In this case, that means RecurseChildren can only be called by GetDebugView().

We'll come back to the RecurseChildren() method shortly, but first let's look at the rest of the GetDebugView() method:

public static string GetDebugView(this IConfigurationRoot root)
{
    void RecurseChildren(StringBuilder stringBuilder, Enumerable<IConfigurationSection> children, string indent) { /* Shown later */ }

    var builder = new StringBuilder();

    RecurseChildren(builder, root.GetChildren(), "");

    return builder.ToString();
}

So the rest of the method

  • Creates a StringBuilder() to hold the output
  • Calls RecurseChildren() to build the output, passing in the StringBuilder and the immediate children of the configuration root.
  • Creates the final string by calling StringBuilder.ToString().

The RecurseChildren method, while it takes up a lot of lines, is using a relatively simple recursive algorithm to walk all the keys in the configuration object:

  • For every key in the section
    • Try and get the value for the key, as well as the provider that that added the key
    • If the key has an associated provider, print the key and value
    • If the key doesn't have a provider, then it must be a "section". Print the section name, fetch the section's children, and call RecurseChildren() over those child keys.

Once the loop over the "top-level" keys is complete, all the configuration values have been iterated and printed to the StringBuilder. The RecurseChildren method uses the indent parameter to keep track of how "deep" into the configuration sections the method is, creating the correct structure for the Logging section for example.

I have written previously about this approach to “Creating an in ASCII art tree in C#”.

The RecurseChildren() method finds the provider and value for a given configuration key using the helper method GetValueAndProvider(). This method is called using the current configuration value, and the implicitly captured IConfigurationRoot variable provided in the GetDebugView() method.

GetDebugView() is an extension method on IConfigurationRoot not on IConfiguration (the interface you typically interact with in ASP.NET Core apps). IConfigurationRoot has access to the underlying configuration providers (in addition to the configuration values themselves), whereas IConfiguration does not.

GetValueAndProvider() iterates through each of the registered configuration providers in reverse, looking for a configuration value with the required key. If the key is found, the associated value and provider are returned. If no provider is found, then the value is inferred to be a "section" with no associated value.

private static (string Value, IConfigurationProvider Provider) GetValueAndProvider(
    IConfigurationRoot root,
    string key)
{
    foreach (IConfigurationProvider provider in root.Providers.Reverse())
    {
        if (provider.TryGet(key, out string value))
        {
            return (value, provider);
        }
    }

    return (null, null);
}

The providers are iterated in reverse because of a feature of the configuration system in ASP.NET Core whereby later configuration providers "overwrite" the values added by earlier configuration providers. This enables you to provide a "default" value in appsettings.json, and then "overwrite" it with an environment variable, for example.

The "reverse" iteration mirrors the way that the ConfigurationRoot iterates providers to find a configuration key.

That covers both the basics and details of GetDebugView(), so finally lets look at a practical way of using it in your applications.

Exposing the debug view in your application

The information provided by GetDebugView() can be very useful when you need to debug a configuration problem in your application—being able to see exactly where a configuration value comes from is invaluable when things aren't working as you expect.

One obvious approach is to expose an endpoint in your ASP.NET Core app where you can query for this debug view. In the following example we use the lightweight MapGet method to expose a simple endpoint:

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            if(env.IsDevelopment())
            {
                endpoints.MapGet("/debug-config", ctx =>
                {
                    var config = (Configuration as IConfigurationRoot).GetDebugView();
                    return ctx.Response.WriteAsync(config);
                });
            }
        });
    }
}

When you call this endpoint, you'll be able to see your application's configuration in the browser:

Debugging configuration in the browser

This is a pretty basic example, but there's a couple of important takeaways:

  • You definitely shouldn't be exposing this data in a production environment. Your config will contain connection strings and secrets, so always place anything like this behind an IsDevelopment() flag!
  • You need to cast the injected IConfiguration to IConfigurationRoot before you can call GetDebugView().

That last point is because although the ConfigurationRoot implementation used in .NET Core 3.0+ implements IConfigurationRoot, it's only registered in the DI container as an IConfiguration object.

Be aware that this is technically a configuration detail, and that there's no reason you will definitely be able to cast an IConfiguration object to IConfigurationRoot. Realistically, it's unlikely that implementation will change significantly though, so it's probably pretty safe.

Exposing this debug view of configuration can be very useful, and it's something I've done often in my applications, but it does make me a little nervous exposing it via an API. In the next post I'll describe another way of exposing these details; using Oakton's Describe command.

Summary

In this post I discussed the IConfigurationRoot.GetDebugView() extension method, and walked through its implementation. This method lists all of the configuration keys and values in your app, as well as the configuration provider that added each key.This can be very useful for working out why a configuration value doesn't have the value you expect, or for spotting typos in your configuration settings. I also described how you can expose this data as an API endpoint using a lightweight API.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?