In my previous post I showed how you could use GetDebugView() to expose a list of your application's configuration keys, values, and their source as an API endpoint. In this post I show an alternative way of exposing that data using Oakton v3's describe command.

IConfigurationRoot.GetDebugView() recap

In my previous post I described the IConfigurationRoot.GetDebugView() extension method, and how it can be used to view all the configuration in your application, including which provider added the configuration value to the collection. The output is a string that looks something like this:

AllowedHosts=* (JsonConfigurationProvider for 'appsettings.json' (Optional))
ALLUSERSPROFILE=C:\ProgramData (EnvironmentVariablesConfigurationProvider)
applicationName=TestApp (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
ASPNETCORE_ENVIRONMENT=Development (EnvironmentVariablesConfigurationProvider)
ASPNETCORE_URLS=https://localhost:5001;http://localhost:5000 (EnvironmentVariablesConfigurationProvider)
contentRoot=C:\repos\TestApp (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))
...

You can generate this string by injecting an IConfiguration instance using the DI container, casting it to an IConfigurationRoot, and calling GetDebugView(), for example:

public class Startup
{
    public void Configure(
        IApplicationBuilder app, 
        IWebHostEnvironment env, 
        IConfiguration config)
    {
        app.UseRouting();
        app.UseAuthorization();

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

In the example above, I created an endpoint that exposes the GetDebugView() result in the Development environment only. Even though we're not exposing the endpoint in production, you still might not be comfortable exposing your sensitive configuration values via an API. For the remainder of this post, I show an alternative approach, using Oakton.

What is Oakton?

Oakton is a library by OSS aficionado Jeremy D. Miller for adding command line options and commands to your applications. It has been my go-to library for creating "helper" CLI tools for my ASP.NET Core applications.

In my recent series about Kubernetes, I described creating helper CLI tools associated with each ASP.NET Core app deployment. We used Oakton to create those CLI tools.

Oakton recently hit version 3 and picked up some great new features. Oakton has always been good at acting s a simple command line parser and command executor for creating CLIs, but now it integrates even more smoothly into your ASP.NET Core or generic host projects. This allows you to add extra features to your ASP.NET Core apps, such as environment checks, while integrating directly into your application.

Jeremy has a great introductory post on Oakton v3 on his website, which I strongly suggest looking into. Alternatively, see the documentation for more details.

In this post I will show how to quickly add Oakton to your ASP.NET Core app with very few changes. I won't show how to create a custom command in this post. Instead we'll extend the built-in describe command.

Adding Oakton to your application

Start by installing the Oakton NuGet package into your project file by running

dotnet add package Oakton

Your project file should look something like the following:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Oakton" Version="3.0.2" />
  </ItemGroup>

</Project>

This adds the Oakton library to your application, enabling the all important RunOaktonCommands() extension method on IHostBuilder. Update your Program.Main() function to replace the existing Build().Run() methods:

public class Program
{
    public static Task<int> Main(string[] args)
    {
        return CreateHostBuilder(args)
            .RunOaktonCommands(args);
            // .Build().Run();

    }

    // Unchanged
    public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
}

With this change, your app now has some extra features. Run dotnet run help in the project folder, and you'll see something like this:

>dotnet run help
Building...

  -----------------------------------------------------------------------------------------------------------------
    Available commands:
  -----------------------------------------------------------------------------------------------------------------
    check-env -> Execute all environment checks against the application
     describe -> Writes out a description of your running application to either the console or a file
         help -> list all the available commands
          run -> Start and run this .Net application
  -----------------------------------------------------------------------------------------------------------------

The help argument here is an Oakton command, which lists out the built-in commands available in Oakton. Other than the help command which you've just used, and the (somewhat superfluous run command, enabling dotnet run run), there's two "real" commands:

  • check-env. This is used to run environment checks, of your application. These are "validation" checks, that you can use to check that you can connect to a database etc.
  • describe. This is used to list details about your application. We'll look in more detail at this command shortly.

In addition to these commands, there are various extra flags you can use to easily set certain configuration values:

 Usages for 'run' (Start and run this .Net application)
  run [-c, --check] [-e, --environment <environment>] [-v, --verbose] [-l, --log-level <loglevel>] [----config:<prop> <value>]

  -----------------------------------------------------------------------------------------------------------------
    Flags
  -----------------------------------------------------------------------------------------------------------------
                        [-c, --check] -> Run the environment checks before starting the host
    [-e, --environment <environment>] -> Use to override the ASP.Net Environment name
                      [-v, --verbose] -> Write out much more information at startup and enables console logging
         [-l, --log-level <loglevel>] -> Override the log level
          [----config:<prop> <value>] -> Overwrite individual configuration items
  -----------------------------------------------------------------------------------------------------------------

We'll leave the flags for now, and take a look at the describe command.

The Describe command

The describe command provides an easy way to list details about your application. This command is extensible (as you'll see shortly), but the built in command shows basic configuration details about your app as well as referenced assemblies:

── About TestApp ───────────────────────────────────────────────────────
          Entry Assembly: TestApp
                 Version: 1.0.0.0
        Application Name: TestApp
             Environment: Development
       Content Root Path: C:\repos\TestApp
AppContext.BaseDirectory: C:\repos\TestApp\bin\Debug\net5.0\


── Referenced Assemblies ───────────────────────────────────────────────
┌───────────────────────────────────────────────────────┬──────────┐
│ Assembly Name                                         │ Version  │
├───────────────────────────────────────────────────────┼──────────┤
│ System.Runtime                                        │ 5.0.0.0  │
│ Microsoft.Extensions.Configuration.UserSecrets        │ 5.0.0.0  │
│ Microsoft.AspNetCore.Mvc.Core                         │ 5.0.0.0  │
│ Oakton                                                │ 3.0.2.0  │
│ Microsoft.Extensions.Configuration.Abstractions       │ 5.0.0.0  │
│ Spectre.Console                                       │ 0.0.0.0  │
│ Microsoft.Extensions.Hosting.Abstractions             │ 5.0.0.0  │
│ Microsoft.AspNetCore.Hosting.Abstractions             │ 5.0.0.0  │
│ Microsoft.Extensions.DependencyInjection.Abstractions │ 5.0.0.0  │
│ Microsoft.AspNetCore.Http.Abstractions                │ 5.0.0.0  │
│ Swashbuckle.AspNetCore.SwaggerGen                     │ 5.6.3.0  │
│ Swashbuckle.AspNetCore.SwaggerUI                      │ 5.6.3.0  │
│ Microsoft.AspNetCore.Routing                          │ 5.0.0.0  │
│ Microsoft.Extensions.Logging.Abstractions             │ 5.0.0.0  │
│ System.ObjectModel                                    │ 5.0.0.0  │
│ Newtonsoft.Json                                       │ 12.0.0.0 │
│ System.ComponentModel.TypeConverter                   │ 5.0.0.0  │
│ System.Linq                                           │ 5.0.0.0  │
│ Microsoft.Extensions.Hosting                          │ 5.0.0.0  │
│ Microsoft.AspNetCore                                  │ 5.0.0.0  │
│ Microsoft.AspNetCore.Mvc                              │ 5.0.0.0  │
│ Microsoft.AspNetCore.Mvc.NewtonsoftJson               │ 5.0.2.0  │
│ Microsoft.AspNetCore.Diagnostics                      │ 5.0.0.0  │
│ Swashbuckle.AspNetCore.Swagger                        │ 5.6.3.0  │
│ Microsoft.AspNetCore.HttpsPolicy                      │ 5.0.0.0  │
│ Microsoft.AspNetCore.Authorization.Policy             │ 5.0.0.0  │
│ Microsoft.AspNetCore.Hosting                          │ 5.0.0.0  │
│ Microsoft.OpenApi                                     │ 1.2.3.0  │
└───────────────────────────────────────────────────────┴──────────┘

As I'm sure you can tell, this could be very useful when you're trying to debug an issue. In the next section we'll extend this command to display additional configuration information.

Creating a custom IDescribedSystemPart

To extend the describe command, create a class that implements IDescribedSystemPart. This interface is pretty simple, requiring just the Title property and the Write(TextWriter) method. The following shows a basic implementation that uses the IConfigurationRoot.GetDebugView() to print the application configuration:

Oakton 3.1 recently added this describer in version 3.1. You can see the implementation here.

using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Oakton.Descriptions;

public class ConfigDescriptionSystemPart: IDescribedSystemPart
{
    readonly IConfigurationRoot _configRoot;
    public ConfigDescriptionSystemPart(IConfiguration config)
    {
        _configRoot = (IConfigurationRoot)config;
    }

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

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

You need to register the extension in your ConfigureServices() method. There's a handy extension method in the Oakton.Descriptions namespace you can use:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDescription<ConfigDescriptionSystemPart>();
}

With the extra description part registered, run the describe command again, using dotnet run -- describe. You'll now see your application's configuration printed to the console before the built-in description parts, using the output shown in my previous post:

── Configuration values and sources ───────────────────────────────────────────────────────────────────────────────
AllowedHosts=* (JsonConfigurationProvider for 'appsettings.json' (Optional))
ALLUSERSPROFILE=C:\ProgramData (EnvironmentVariablesConfigurationProvider)
applicationName=TestApp (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
ASPNETCORE_ENVIRONMENT=Development (EnvironmentVariablesConfigurationProvider)
ASPNETCORE_URLS=https://localhost:5001;http://localhost:5000 (EnvironmentVariablesConfigurationProvider)
contentRoot=C:\repos\TestApp (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))
...

Oakton also supports creating nicer console output using Spectre.Console, by implementing the interface IWriteToConsole. This adds an additional method to implement, WriteToConsole().

The following shows how you could extend the ConfigDescriptionSystemPart to generate a nicer "tree view" output. The implementation is virtually identical to the original GetDebugView() implementation I walked through in my previous post, using a recursive method to build up the tree, but building a Spectre.Console Tree instead of a simple string.

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

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)
            {
                (string Value, IConfigurationProvider Provider) valueAndProvider = GetValueAndProvider(_configRoot, child.Path);

                IHasTreeNodes parent = node;
                if (valueAndProvider.Provider != null)
                {
                    node.AddNode(new Table()
                        .Border(TableBorder.None)
                        .HideHeaders()
                        .AddColumn("Key")
                        .AddColumn("Value")
                        .AddColumn("Provider")
                        .HideHeaders()
                        .AddRow($"[yellow]{child.Key}[/]", valueAndProvider.Value, $@"([grey]{valueAndProvider.Provider}[/])")
                    );
                }
                else
                {
                    parent = node.AddNode($"[yellow]{child.Key}[/]");
                }

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

        var tree = new Tree(string.Empty);

        RecurseChildren(tree, _configRoot.GetChildren());

        AnsiConsole.Render(tree);

        return Task.CompletedTask;
    }

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

Instead of the plain-text version of configuration values, you'll get a nice, tree-view version instead:

── Configuration values and sources ───────────────────────────────────────────────────────────────────────────────
├── AllowedHosts *     (JsonConfigurationProvider for 'appsettings.json' (Optional))
├── ALLUSERSPROFILE C:\ProgramData (EnvironmentVariablesConfigurationProvider)
├── applicationName TestApp  (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
├── ASPNETCORE_ENVIRONMENT Development (EnvironmentVariablesConfigurationProvider)
├── ASPNETCORE_URLS https://localhost:5001;http://localhost:5000 (EnvironmentVariablesConfigurationProvider)
├── contentRoot C:\repos\TestApp (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
├── DOTNET_ROOT C:\Program Files\dotnet (EnvironmentVariablesConfigurationProvider)
├── ENVIRONMENT Development (Microsoft.Extensions.Configuration.ChainedConfigurationProvider)
├── 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))
│   └── LogLevelDefault Warning (JsonConfigurationProvider for 'secrets.json' (Optional))
├── MySecretValue TOPSECRET (JsonConfigurationProvider for 'secrets.json' (Optional))

In my implementation I decided to use some of the markup features of Spectre.Console to make the output a little easier to read, adding some colour to the result:

Example of Oakton Describe with the configuration values

I think this could be quite a handy addition to the built in describe command for visualizing the keys in your application. Now I'm trying to think about what other extensions to create!

Summary

In this post I gave a brief introduction to Oakton, and the describe command available in version 3. The describe command provides an easy way to view detail about your application's configuration. By default, it shows details about your app's IWebHostEnvironment and the referenced assemblies.

In this post, I showed how you could extend the describe command by creating a custom IDescribedSystemPart that prints your app configuration using the GetDebugView() extension method I discussed in my previous post. I then showed how you could print a prettier version by using the built-in support for Spectre.Console.