Monday, May 4, 2020

Configuration in .NET Core console applications

If you search the official .NET documentation, you will probably not find much information on how to add config files to your .NET Core console applications. Let's learn how.
Photo by Christopher Gower on Unsplash

With the release of .NET Core 3.1, Microsoft changed a few things in how we access configuration in our files. While with ASP.NET documentation is really solid and scaffolding an ASP.NET Core website should include all the dependencies to get that right, the same does not happen with Console Applications. On this quick  tutorial let's see how we can replicate the same setup for our console apps.

Why replicate ASP.NET Configuration

The maturity that the .NET Core framework achieved includes the configuration framework. And all of that, despite the lack of documentation, can be shared between web and console apps. That said, here are some reasons why you should be using some of the ASP.NET tolling on your console projects:
  • the configuration providers read configuration data from key-value pairs using a variety of configuration sources including appsettings.json, environment variables, and command-line arguments
  • it can be used with custom providers
  • it can be used with in-memory .NET objects
  • if you're developing with Azure, integrates with Azure Key Vault, Azure App Configuration 
  • if you're running Docker, you can override your settings via the command line or environment variables
  • you will find parsers for most formats (we'll see an example here)

The Solution

So let's take a quick look at how to integrate some of these tools in our console apps.

Adding NuGet packages

Once you create your .NET Core app, the first thing to do is to add the following packages:
  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Binder
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json
Next, add the following initialization code:
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var builder = new ConfigurationBuilder()
    .AddJsonFile($"appsettings.json", true, true)
    .AddJsonFile($"appsettings.{env}.json", true, true)
    .AddEnvironmentVariables();

var config = builder.Build();
If set, the env var above will auto-load the configuration as per the environment variable ASPNETCORE_ENVIRONMENT that comes preset on a new ASP.NET Core project. So for dev, it will try to use appSettings.Development.json sticking with appSettings.Development.json if the former doesn't exist.

Creating a configuration file

Now add an empty appSettings.json file in the root of your project and add your configuration. Remember that this is a json file so your config should be a valid json document. For example, to config file for one of my microservices is:
{
  "MassTransit": {
    "Host": "rabbitmq://localhost",
    "Queue": "hildenco"
  },
  "ConnectionString": "Server=localhost;Database=hildenco;Uid=<username>;Pwd=<pwd>",
  "Smtp": {
    "Host": "<smtp-server>",
    "Port": "<smtp-port>",
    "Username": "<username>",
    "Password": "<password>",
    "From": "HildenCo Notification Service"
  }
}

Parsing the configuration

There are two ways to access the configuration: by accessing each entry individually or by mapping the whole config file (or specific sections) to a class of our own. Let's see both.

Accessing config entries

With the config instance above, accessing our configurations is now simple. For example, accessing a root property is:
var appName = config["ConnectionString"];
While accessing a sub-property is:
var rmqHost = config["RabbitMQ:Host"];

Mapping the configuration

Despite working well, the previous example is verbose and error prone. So let's see a better alternative: mapping the configuration to a POCO class that Microsoft calls the options pattern. Despite its fancy name, it's probably something that you'll recognize.

We'll also see two examples: mapping the whole configuration and mapping one specific section. For both, the procedure will require these steps:
  • creating an options file
  • mapping to/from the settings
  • binding the configuration.

Mapping the whole config

Because our configuration contains 3 main sections - MassTransit, a MySQL ConnectionString and a SMTP config -, we'll model our AppConfig file the same way:
public class AppConfig
{
    public SmtpOptions Smtp { get; set; }
    public MassTransitOptions MassTransit { get; set; }
    public string ConnectionString { get; set; }
}
SmtpOptions should also be straight-forward:
public class SmtpOptions
{
    public string Host { get; set; }
    public int  Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}
As MassTransitOptions:
public class MassTransitOptions
{
    public string Host { get; set; }
    public string Queue { get; set; }
}
The last step is binding the whole configuration with our config:
var cfg = config.Get<AppConfig>();

Accessing Configuration Properties

With the config loaded, accessing our configs becomes trivial:
var cs = cfg.ConnectionString;
var smtpFrom = cfg.Smtp.From;

Mapping a Section

To map a section we use the method .GetSetcion("<section-name>").Bind() present on the Microsoft.Extensions.Configuration.Binder NuGet package that we added earlier. For example, to map just SmtpOptions we'd do:
var mailOptions = new SmtpOptions();
config.GetSection("Mail").Bind(mailOptions);

Making it Generic

Turns out that quickly the previous procedure also gets verbose. So let's shortcut it all with the following generic method (static if ran from Program.cs):
 private static T InitOptions<T>(string section)
    where T : new()
{
    var config = InitConfig();
    return config.GetSection<T>(section);
}
And using it with:
var smtpCfg = InitOptions<SmtpConfig>("Smtp");

Reviewing the solution

Everything should be good at this point. Remember to leverage your options classes along with your Dependency Injection framework instead of accessing the IConfiguration for performance reasons. To conclude, here's our final program.cs file:
static async Task Main(string[] args)
{
    var cfg = InitOptions<AppConfig>();
    // ...
}

private static T InitOptions<T>()
    where T : new()
{
    var config = InitConfig();
    return config.Get<T>();
}

private static IConfigurationRoot InitConfig()
{
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    var builder = new ConfigurationBuilder()
        .AddJsonFile($"appsettings.json", true, true)
        .AddJsonFile($"appsettings.{env}.json", true, true)
        .AddEnvironmentVariables();

    return builder.Build();
}

Conclusion

On this post we reviewed how to use the ASP.NET tooling to bind and access configuration from our console applications. While .NET Core matured a lot, the documentation for console applications is not that great. For more information on the topic I suggest reading about Configuration in ASP.NET Core and understanding .NET Generic Host.

References

See Also

About the Author

Bruno Hildenbrand      
Principal Architect, HildenCo Solutions.