It’s all in the Host Class – Part 2: Configuration

In part 1 of this article series Dependency Injection was covered – one of the features of the Host class. This article shows another aspect that’s needed by nearly every application: configuration. Here, I’m covering using the ConfigurationBuilder, using the IConfiguration API, injecting this interface, and what’s in the Host class.

Ferrari Steering Wheel

Overview

With the old .NET Framework, in Web applications, configuration values are stored in the XML file web.config. This file not only contains application settings, but a lot more such as redirects of assemblies based on versions, and runtime configuration. To allow having different configuration values for staging and production servers, and not the need to create copies of complete files (and to miss some changes in future versions), XML transformations can be done – and are supported with Visual Studio.

.NET in its actual version offers more flexibility, it’s easier to use, and it’s more powerful with configurations. You can use different providers to store application settings in JSON files, environmental variables, and command line arguments, and easily add other providers such as having the configuration stored with Azure App Configuration or Azure Key Vault.

Let’s get into an example.

Using Simple Configuration

The first code snippet is part of a .NET Core console application, and the configuration setup defines to read configuration from the JSON file appsettings.json. The NuGet package Microsoft.Extensions.Configuration is needed for all the configuration types, such as the ConfigurationBuilder. To read the configuration file from JSON, another NuGet package, Microsoft.Extensions.Configuration.Json is required. Creating a new ConfigurationBuilder instance, a Fluent API is offered. The SetBasePath method defines the directory where the configuration files are read from there on. The AddJsonFile extension method that has been made available from the Microsoft.Extensions.Configuration.Json package defines the filename for the configuration file. SetBasePath needs to be invoked before the invocation of the AddJsonFile. In case you want to read values from configuration files in different folders, SetBasePath needs to be invoked multiple times – always before defining the file itself. After this filename is configured, the Build method is invoked which returns an IConfigurationRoot object. This returned object is returned from the SetupSimpleConfiguration method.

private static IConfiguration SetupSimpleConfiguration()
  => new ConfigurationBuilder()
      .SetBasePath(Directory.GetCurrentDirectory())
      .AddJsonFile("appsettings.json")
      .Build();

> The Build method that is defined by the IConfigurationBuilder interface returns an IConfigurationRoot, wheras the method SetupSimpleConfiguration returns IConfiguration. IConfigurationRoot derives from IConfiguration and adds the Providers property and the Reload method. The Providers property returns a list of configuration configuration providers. The Reload method can be invoked to refresh configuration values when they probably have been changed in the file system while the application was running.

The configuration file applicationsettings.json defines a simple configuration value for the key SimpleConfig:

{
  "SimpleConfig": "SimpleValue"
}

To read the configuration value, all what’s needed is to use the indexer of the IConfiguration interface and pass the name of the key:

private static void ReadSimpleConfiguration(IConfiguration configuration)
{
    Console.WriteLine(nameof(ReadSimpleConfiguration));
    string val1 = configuration["SimpleConfig"];
    Console.WriteLine($"Read {val1} using the key SimpleConfig");
    Console.WriteLine();
}

The variable val1 now contains the string SimpleValue and is shown on the console.

Using Different Configuration Providers

There’s no need to store your configuration values within JSON files. You can use XML files, also can make use of INI files, pass configuration files with command line arguments, or environmental variables – you can use any provider for configuration values, or create your own. To use configuration files from XML files, the NuGet package Microsoft.Extensions.Configuration.Xml can be used, with environmental variables the package Microsoft.Extensions.Configuration.EnvironmentVariables.

private static IConfiguration SetupConfigurationWithMultipleProviders(string[] args) =>
    new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json")
        .AddEnvironmentVariables()
        .AddCommandLine(args)
        .Build();

To run the application from Visual Studio, the program arguments as well as the environmental variables can be set from the Debug settings of the project properties:

Visual Studio Debug Settings

> Using the same configuration keys with multiple providers, the order how the providers are added to the ConfigurationBuilder becomes important. The provider that is added last wins.

Defining Different Values for Development, Staging, Production

With the new configuration it is easy to supply different configuration values for development, staging, and production environments. With the sample project I’ve configured the environment variable DOTNET_Environment to the name of the environment, e.g. Staging. This environmental variable is used to define the filename for the configuration. appsettings.json is used for the configuration that is not different with the servers, with the development server the additional configuration file appsettings.development.json is used, with the production server appsettings.production.json. By default, if the file does not exist, an exception is thrown. Passing true with the second argument of AddJsonFile, it can be specified that the setting is optional.

private static IConfiguration SetupConfigurationWithOptionalSettings()
{
    string environment = Environment.GetEnvironmentVariable("DOTNET_Environment");

    string environment = Environment.GetEnvironmentVariable("DOTNET_Environment") ?? "Production";

    return new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json")
        .AddJsonFile($"appsettings.{environment}.json", optional: true)
        .Build();
}

Reading from Sections

With the previous sample, no hierarchy was used. With JSON files it is also possible to define hierarchical levels. For example, the following JSON file defines a section ConnectionStrings, and the connection named DefaultConnection within this section:

{
  "ConnectionStrings": {
    "DefaultConnection": "Connection string to the staging database"
  }
}

To read the connection string, the GetSection method can be used passing the section name. The GetSection method returns an IConfigurationSection where in turn again the indexer can be used to retrieve the values within this section. For easier use of connection strings, the extension method GetConnectionString exists that does the same – retrieving a key from the section ConnectionStrings.

private static void ReadConfigurationWithOptionalSettings(IConfiguration configuration)
{
    Console.WriteLine(nameof(ReadConfigurationWithOptionalSettings));
    Console.WriteLine(configuration.GetSection("ConnectionStrings")["DefaultConnection"]);
    Console.WriteLine(configuration.GetConnectionString("DefaultConnection"));
    Console.WriteLine();
}

Using the Host class

Using the Host class and invoking the method CreateDefaultBuilder a lot of the configuration is already done. This method adds these configuration providers:

  • JSON file appsettings.json
  • JSON provider appsettings.{environment}.json
  • Provider for environmental variables
  • Provider for command line arguments (in case the command-line arguments are passed to the CreateDefaultBuildermethod)
  • User secrets when user secrets are configured

> Storing secrets in a source code repository is not a good idea. Here, user secrets help. With user secrets, the configuration is stored in the user profile. Of course, this option is only available during development.

With the second sample application, the Controller class is created which expects that the IConfiguration interface is injected. The variable where the object implementing the interface is assigned to is used in the method ReadConfigurationValues:

public class Controller
{
    private readonly IConfiguration _configuration;
    public Controller(IConfiguration configuration) =>
        _configuration = configuration;

    public void ReadConfigurationValues()
    {
        var config1 = _configuration["Config1"];
        Console.WriteLine($"config1: {config1}");

        var connectionString = _configuration.GetConnectionString("DefaultConnection");
        Console.WriteLine(connectionString);
    }
}

To setup the Host class, the static method CreateDefaultBuilder is invoked. This method returns an IHostBuilder. This IHostBuilder is used to configure the dependency injection container (DI) calling the ConfigureServices method. The Controller class is registered, so that the container can inject the IConfiguration interface. The IConfiguration interface is one of the services registered with the DI containers method CreateDefaultBuilder. The Build method returns the host implementing the IHost interface. The host variable is then used to access the Controller, and invoke the ReadConfigurationValues method to display the configuration values.

static void Main(string[] args)
{
    using var host = Host.CreateDefaultBuilder(args)
        .ConfigureServices(services =>
        {
            services.AddTransient();
        })
        .Build();

    var controller = host.Services.GetRequiredService();
    controller.ReadConfigurationValues();
}

All what now needs to be done is to create the appsettings.json file and configure the values (or to use environmental variables or command line arguments), and read retrieve the values.

Strongly Typed Access

For strongly typed access, .NET classes can be specified to define what configuration values are needed, such as the MyConfiguration class and the InnerConfiguration class. MyConfiguration defines properties for string and int values, as well as the contained Inner property to access values specified by InnerConfiguration:

public class InnerConfiguration
{
    public string InnerText { get; set; } = string.Empty;
}

public class MyConfiguration
{
    public string Text1 { get; set; } = string.Empty;
    public int Number1 { get; set; }

    public InnerConfiguration Inner { get; } = new InnerConfiguration();
}

The configuration values are defined with keys having the same name as the properties defined:

{
  "Config2": "from appsettings.json",
  "ConnectionStrings": {
    "DefaultConnection": "Connection string to the default database"
  },

  "MyGroup1": {
    "Text1": "value for text1",
    "Number1": 42,
    "Inner": {
      "InnerText": "value for inner text"
    }
  }
}

Reading the configuration values, it’s possible to bind the retrieved values to this class. In the Controller class, an instance of the MyConfiguration class is created, and using the IConfiguration interface, and the values are assigned to the properties:

public void StronglyTypedConfiguration()
{
    var settings = new MyConfiguration();
    _configuration.GetSection("MyGroup1").Bind(settings);

    Console.WriteLine($"text: {settings.Text1}");
    Console.WriteLine($"number: {settings.Number1}");
    Console.WriteLine($"inner text: {settings.Inner.InnerText}");
}

Using Configuration with Options

In the previous blog article of this series I’ve shown how to use the IOptions interface to pass initialization data to a service class injected by using the DI container. The same initialization can be used to pass configuration data.

public class ControllerWithOptions
{
    private readonly IOptions _options;
    public ControllerWithOptions(IOptions options) => 
        _options = options;

    public void StronglyTypedConfiguration()
    {
        Console.WriteLine($"text: {_options.Value.Text1}");
        Console.WriteLine($"number: {_options.Value.Number1}");
        Console.WriteLine($"inner text: {_options.Value.Inner.InnerText}");
    }
}

Using a Configure extension method of the IServiceCollection interface, an object implementing the IConfiguration interface can be passed. To make this easy to combine it with the type ControllerWithOptions where the configuration data needs to be set, the extension method AddControllerWithOptions is specified:

public static class ControllerWithOptionsExtensions
{
    public static IServiceCollection AddControllerWithOptions(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.Configure(configuration);
        return services.AddTransient();
    }
}

On configuring of the Host class, the IConfiguration interface can now be passed retrieving the section MyGroup1 where the configuration values are stored. Because in this case accessing configuration information is needed on configuration of the services, an overload of ConfigureServices is used where the HostBuilderContext is the first parameter. Using this parameter, the configuration already defined by the Host class can be accessed using the Configuration property. Using the configuration variable, configuration is retrieved with the GetSection method. This method returns IConfigurationSection which itself derives from the base interface IConfiguration, and thus can be passed to the AddControllerWithOptions method.

using var host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((context, services) =>
    {
        var configuration = context.Configuration;
        services.AddControllerWithOptions(configuration.GetSection("MyGroup1"));
    })
.Build();

Customizing configuration with the Host class

To add other configuration providers to .NET applications, the only thing needed are to add a NuGet package for the configuration provider, and to configure the provider with the ConfigureAppConfiguration method, as shown in Azure App Configuration: Configuration of .NET Applications where the Azure App Configuration provider is added.

Take away

Configuration with .NET is very flexible in that it supports different configuration providers – no matter if the configuration is coming from JSON files, XML or INI files, environmental variables or command line arguments. The Host class defines preconfigured configuration. Built-in from the Host class is support to inject IConfiguration with services, so service implementations can directly access values coming from configuration. Configuration can also be passed to a service when configuring the Host class injecting the IOptions interface with the service, and accessing configuration data using an overload of the ConfigureServices method accessing the HostBuilderContext.

If you’ve read this far, consider buying me a coffee which helps me staying up longer and writing more articles.

Buy Me A Coffee

ou can get the complete sample code. See the ConfigurationWithHost sample solution in the Dotnet folder.

Enjoy learning and programming!

Christian

More Information

It’s all in the Host Class – Dependency Injection with .NET

Azure App Configuration: Configuration of .NET Applications

Azure Functions with Dependeny Injection

Disposing Injected Services

HTTP Client Factory with .NET Core

Implementation of the CreateDefaultBuilder Method on GitHub

More information on C# and programming .NET Core applications is in my book Professional C# 7 and .NET Core 2.0, and in my workshops.

Formula 1 Steering Wheel ID 143423851 © Logan Chislett | Dreamstime.com

3 thoughts on “It’s all in the Host Class – Part 2: Configuration

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.