In a previous post I showed how you could create your own version of the IConfigurationRoot.GetDebugView() extension method, to visualize the configuration values in your ASP.NET Core app.

In this post I continue on that path, adding additional functionality to the view—showing the overwritten values, as well as the final values.

I'm not the only person to think of this extension, Phil Scott posted a (very!) similar approach in a gist.

Recap: viewing configuration values in ASP.NET Core

I recently discovered the IConfigurationRoot.GetDebugView() extension method, thanks to a tweet from Cecil Phillip. This method returns a string listing the configuration keys in your IConfigurationRoot object, their value, and which provider the value came from:

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))

It's handy having this quick method available in the libraries, but it would also be good to have a slightly nicer view of it.

In my previous post, I showed how you could use the open source Oakton library for building CLI commands, extending the built-in DescribeCommand to also display your application's configuration using a nice tree view instead:

Using Oakton's describe command to display application configuration

At this point, we've replicated the functionality of GetDebugView(), and just improved the display somewhat. In this post we'll add some new functionality.

Configuration in ASP.NET Core: layers of configuration

The functionality we want to add is to show the overwritten values for each key.

ASP.NET Core uses a "layered" approach to configuration. You can add multiple configuration providers, each of which adds values from a specific source, for example JSON files, environment variables, Key Vault etc.

The order that the configuration providers are added is important, as configuration values added by later providers will overwrite the values added by earlier providers.

The later configuration providers (Environment Variables) overwrite the values added by earlier configuration providers (appsettings.json, sharedsettings.json). Taken from my book, ASP.NET Core in Action, Second Edition

This is especially useful for providing "default" values, used during development (for example, connection strings to a local database), which are then overridden in production with the production values.

One of the main features of the GetDebugView() extension method is that it shows you which configuration provider added the configuration value that was used. In this post, we're going to extend that approach to show the values that were overridden too.

Extending the tree view to show all configuration values

For simplicity, I'm going to carry on where I left off on the last post, with the ConfigDescriptionSystemPart that can be used with Oakton to describe the configuration values in your application.

If you haven't already, I suggest you take a look at that post where I talk more about Oakton and the DescribeCommand.

The end result we're aiming for is shown below. In this example, you can see that some values are shown in strikethrough, indicating that they were overridden, for example the Logging:LogLevel:Default and MySecretValue keys.

Example of Oakton Describe with the configuration values showing overwritten values

As in the previous post, I'll start by showing the complete code, and will then walk through it afterwards:

using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Oakton.Descriptions;
using Spectre.Console;

namespace OaktonDescribe
{
    public class ConfigDescriptionSystemPart: IDescribedSystemPart, IWriteToConsole
    {
        readonly IConfigurationRoot _configRoot;
        public ConfigDescriptionSystemPart(IConfiguration config)
        {
            _configRoot = config as IConfigurationRoot;
        }

        public string Title => "Configuration values and sources";

        public Task Write(TextWriter writer)
        {
            return writer.WriteAsync(_configRoot.GetDebugView());
        }

        public Task WriteToConsole()
        {
            void RecurseChildren(IHasTreeNodes node, IEnumerable<IConfigurationSection> children)
            {
                foreach (IConfigurationSection child in children)
                {
                    var valuesAndProviders = GetValueAndProviders(_configRoot, child.Path);

                    IHasTreeNodes parent = node;
                    if (valuesAndProviders.Count == 0)
                    {
                        parent = node.AddNode($"[blue]{child.Key}[/]");
                    }
                    else
                    {
                        var finalValue = valuesAndProviders.Pop();
                        var currentNode = node.AddNode(new Table()
                            .Border(TableBorder.None)
                            .HideHeaders()
                            .AddColumn("Key")
                            .AddColumn("Value")
                            .AddColumn("Provider")
                            .HideHeaders()
                            .AddRow($"[yellow]{child.Key}[/]", finalValue.Value, $@"([grey]{finalValue.Provider}[/])")
                        );

                        // Loop through the remaining (overridden) values
                        // Display them as children of the current value
                        foreach (var overriddenValue in valuesAndProviders)
                        {
                            currentNode.AddNode(new Table()
                                .Border(TableBorder.None)
                                .HideHeaders()
                                .AddColumn("Value")
                                .AddColumn("Provider")
                                .HideHeaders()
                                .AddRow($"[strikethrough]{overriddenValue.Value}[/]", $@"([grey]{overriddenValue.Provider}[/])")
                            );
                        }
                    }

                    RecurseChildren(parent, child.GetChildren());
                }
            }

            var tree = new Tree(string.Empty);

            RecurseChildren(tree, _configRoot.GetChildren());

            AnsiConsole.Render(tree);

            return Task.CompletedTask;
        }

        private static Stack<(string Value, IConfigurationProvider Provider)> GetValueAndProviders(
            IConfigurationRoot root,
            string key)
        {
            var stack = new Stack<(string, IConfigurationProvider)>();
            foreach (IConfigurationProvider provider in root.Providers)
            {
                if (provider.TryGet(key, out string value))
                {
                    stack.Push((value, provider));
                }
            }

            return stack;
        }
}

The IDescribedSystemPart interface and WriteToConsole()

Starting at the top of the class, we have the Title property and the Write() method, required by the IDescribedSystemPart interface. I've used the built-in GetDebugView() method for simplicity here, as my focus is the WriteToConsole() method that uses Spectre.Console.

The structure of the WriteToConsole() method is actually the same as the last post:

public Task WriteToConsole()
{
    void RecurseChildren(IHasTreeNodes node, Enumerable<IConfigurationSection> children) { /* body shown below */ }

    var tree = new Tree(string.Empty);

    RecurseChildren(tree, _configRoot.GetChildren());

    AnsiConsole.Render(tree);

    return Task.CompletedTask;
}

This method uses a local function, RecurseChildren(), to do the bulk of the work, which we'll look at shortly. Otherwise, we create a new Spectre.Console Tree which we use to build the configuration structure, and pass it to the RecurseChildren() method. This populates the tree, which we then render to the console using Spectre.Console's AnsiConsole.Render method.

RecurseChildren: visiting every configuration key

Now let's look at the RecurseChildren() method, where the bulk of the work happens:

void RecurseChildren(IHasTreeNodes node, IEnumerable<IConfigurationSection> children)
{
    foreach (IConfigurationSection child in children)
    {
        var valuesAndProviders = GetValueAndProviders(_configRoot, child.Path);

        IHasTreeNodes parent = node;
        if (valuesAndProviders.Count == 0)
        {
            // Doesn't have a value, so must be a "section" heading
            parent = node.AddNode($"[blue]{child.Key}[/]");
        }
        else
        {
            // Has a value, so print them out. 
            // Shown below
        }

        RecurseChildren(parent, child.GetChildren());
    }
}

The RecurseChildren() function uses a combination of iteration and recursion to visit all of the keys in the IConfigurationRoot object. It starts by calling GetValueAndProviders (shown in the section below), to see if the current key (child.Path) has a value. If there aren't any values (i.e. we have a key but no value), then the current node is a "section", such as the Logging and Logging:LogLevel keys from the example earlier in this post.

If the current key is a section, then we add a new node for it to the tree (in blue), and set the parent to the new node. This ensures that we recurse down loop through all the section headers in the configuration object, printing them all out.

If we do have a configuration value for the key, then we will display the details, but first, let's look at the GetValueAndProviders() method that gets the values for a given key.

Getting all the configuration values for a key with GetValueAndProviders()

In the GetDebugView() method, we find the configuration value for a key by iterating the configuration providers in reverse, and exiting as soon as we find the key. However, we now want to see all the configuration values for a given key, including the overridden values from "lower" layers of configuration. Instead of returning a single value/provider pair, we now return a Stack<T> of them instead.

Stack<> is a "last-in-first-out" (LIFO) data structure. I chose to use a Stack over a simple List<> as we're going to display the values in reverse in the RecurseChildren() method. But you could also stick to IEnumerable<T> and iterate the collection in reverse if you prefer.

private static Stack<(string Value, IConfigurationProvider Provider)> GetValueAndProviders(
    IConfigurationRoot root, string key)
{
    // Return matching values from all providers, not just the final value
    var stack = new Stack<(string, IConfigurationProvider)>();

    foreach (IConfigurationProvider provider in root.Providers)
    {
        if (provider.TryGet(key, out string value))
        {
            stack.Push((value, provider));
        }
    }

    return stack;
}

Note that as we're using a Stack<> here, we don't need to iterate the configuration providers in reverse, unlike in the previous post and the original GetDebugView() implementation.

Now that we're returning a Stack<> from GetValueAndProviders we can display both the current value, and the overridden values in RecurseChildren():

// Remove the last value added to the stack. This is the "current" value
var finalValue = valuesAndProviders.Pop();
var currentNode = node.AddNode(new Table()
    .Border(TableBorder.None)
    .HideHeaders()
    .AddColumn("Key")
    .AddColumn("Value")
    .AddColumn("Provider")
    .HideHeaders()
    .AddRow($"[yellow]{child.Key}[/]", finalValue.Value, $@"([grey]{finalValue.Provider}[/])")
);

// Loop through the remaining (overridden) values
// Display them as children of the current value
foreach (var overriddenValue in valuesAndProviders)
{
    currentNode.AddNode(new Table()
        .Border(TableBorder.None)
        .HideHeaders()
        .AddColumn("Value")
        .AddColumn("Provider")
        .HideHeaders()
        .AddRow($"[strikethrough]{overriddenValue.Value}[/]", $@"([grey]{overriddenValue.Provider}[/])")
    );
}

We start by Pop()-ing a value off the Stack<> of configuration values. This is the last value that was added, and therefore represents the "final" value of the configuration. We add a new node to the Tree, displaying the configuration key in yellow, the value in white, and the provider that gave the value in grey.

I used an "invisible" Table inside the node (no borders or headers), as I preferred the layout for keys with long values, but this is entirely optional!

With the final values written, we now loop through the remaining configuration values in the stack (if any), and write out the overridden values as children of the final value, printing the value in strikethrough font, to make it clear that it's overridden.

And that's all there is to it. We can try out the method by running dotnet run -- describe to run Oakton's DescribeCommand, and we see that the values are printed as expected:

Example of Oakton Describe with the configuration values showing overwritten values

That's about all I could think to do with my new found configuration debugging powers. Is there anything else you can think of?

Summary

In this post I showed how you could create your own custom version of IConfigurationRoot.GetDebugView() that shows the configuration values that have been overridden by later configuration providers. I showed how you could extend Oakton's DescribeCommand using this approach, and use Spectre.Console to create a clean tree-view of the output.