by Matthew Thornton
28 December, 2016 - 5 minute read

Here at Winton we like our application configuration to be:

That’s why we have chosen to use git as the configuration source, Consul as the delivery mechanism and git2consul as the bridge between them. There’s a great blog post here about why git2consul was developed to provide a configuration system that meets the above requirements.

image-net

We also like to write many of our web services using Asp.Net Core, the new open source and cross platform version of Asp.Net. So the only missing ingredient was a means of loading configuration from Consul in an Asp.Net Core app. Luckily, Asp.Net Core has an excellent, extensible and open source configuration module.

This made it really easy to write an extension to load config from Consul. We’ve decided to give this code back to the open source community as a package called Winton.Extensions.Configuration.Consul. The project is up on GitHub and the package is distributed via NuGet.

Installation

If you haven’t set up Consul before, there’s an easy to follow guide on their website on how to get up and running. Just add the package Winton.Extensions.Configuration.Consul to the dependencies section of your app’s project.json file like so:

"dependencies": {
    "Winton.Extensions.Configuration.Consul: "*"
} 

Usage

Winton.Extensions.Configuration.Consul exposes an extension method to the ConfigurationBuilder in the same way that the configuration providers in Microsoft.Extensions.Configuration do. The snippet below shows the minimal setup necessary for an Asp.Net application:

public class Startup
{
    private readonly CancellationTokenSource _consulCancellationSource = new CancellationTokenSource();
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddConsul(
                $"{env.ApplicationName}/{env.EnvironmentName}/appsettings.json",
                _consulCancellationSource.Token);
       Configuration = builder.Build();
    }
 
    public IConfigurationRoot Configuration { get; }
 
    // Rest of the Startup class
} 

In its simplest form we just need to pass it the name of the key under which our configuration is stored in Consul and a CancellationToken.

We can also make use of the IHostingEnvironment that is injected into Startup to create a key that changes dynamically in each environment that our application runs in. This provides an easy way to separate config for development, staging and production environments for example. You can read more about different environments in Asp.Net here.

Assuming we have an instance of Consul running on localhost and listening on the default port. Then, if our application was called Website and it was running in the Staging environment, this would load our configuration by reading the value from the key Website/Staging/appsettings.json and parse it as if it were a json file.

Finally, we need to ensure that we cancel any pending requests with Consul when the application is shutting down. This is easily done by registering a method on IApplicationLifetime.ApplicationStopping in the Configure method of Startup.

It looks something like this:


public class Startup
{
    // Rest of Startup class
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
    {
        // Rest of Configure method
 
        appLifetime.ApplicationStopping.Register(_consulCancellationSource.Cancel);
    }
} 

Further Configuration Options

An overload for AddConsul is also provided that takes as a third parameter an Action<IConsulConfigurationSource>options. This allows us to specify a multitude of options, including a different host or port for the instance of Consul we would like to connect to, or how we would like to handle exceptions that might be thrown or a different file parser to use for, say, XML or YAML. For more details see the documentation on the GitHub project site.

Reloading on Changes

The options parameter also allows us to specify if we would like to watch the key in Consul for changes and automatically reload the configuration when this happens. To do this we just change the code in Startup as follows:

public class Startup
{
    private readonly CancellationTokenSource _consulCancellationSource = new CancellationTokenSource();
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddConsul(
                $"{env.ApplicationName}/{env.EnvironmentName}/appsettings.json",
                _consulCancellationSource.Token,
                options =>
                {
                    options.ReloadOnChange = true;
                });
       Configuration = builder.Build();
    }
 
    // Rest of the Startup class
} 

Then we can use the static ChangeToken.OnChange method from Microsoft.Extensions.Primitives to react to configuration changes as follows:


ChangeToken.OnChange(
    Configuration.GetReloadToken,
    () => logger.LogInformation("Config Reloaded!")
); 

This is just how we would do it if we were using any of the configuration providers found in Microsoft.Extensions.Configuration. The configuration sources are nicely decoupled from how they are used within the application; the rest of the application doesn’t need to know how the configuration was loaded.

A word of caution here though. If you’re using strongly typed config options from Microsoft.Extensions.Options then you’ll need to re-bind your objects manually on reload. This is a problem that applies to .NET config in general, whatever provider you’re using.

Further Examples

If you’d like a more complete example of this library in action, check out the example website in the project on GitHub.

Contributing

We’re giving this project back to the open source community by putting it up on GitHub. We’d love your input, feedback and contributions. If you have ideas for improvements, or if you’ve spotted a bug, then please open an issue and we can work together to get your features and fixes included. For more information on contributing to the project see the contributing guidelines.

Do you like to contribute code, but don’t have any specific improvements in mind? At present this library only supports config in json format, but we’d love to support more formats out of the box. All you need to do is implement Winton.Extensions.Configuration.Consul.Parsers.IConfigurationParser for your config format of choice and open a pull request.

Conclusion

Having the configuration files stored in a Git gives us change tracking and versioning of our configuration, and by putting it in a separate repository to the application’s source code it can be owned by the business and updated independently of application releases.

Consul then gives us a robust, scalable means of accessing this configuration and git2consul takes care of ensuring that when git is updated, those changes are available in Consul.

Finally, this library provides the last piece of the puzzle by allowing us to leverage this configuration system in our .NET Core applications.