November 20, 2016

Using .NET Core Configuration with legacy projects

In .NET Core, configuration has been re-engineered, throwing away the System.Configuration model that relied on XML-based configuration files and introducing a number of new configuration components offering more flexibility and better extensibility.

At its lowest level, the new configuration system still provides access to key/value based settings. However, it also supports multiple configuration sources such as JSON files, and probably my favourite feature, strongly typed binding to configuration classes.

Whilst the new configuration system sit unders the ASP.NET repository on GitHub, it doesn’t actually have any dependency on any of the new ASP.NET components meaning it can also be used in your non .net core projects too.

In this post I’ll cover how to use .NET Core Configuration in an ASP.NET Web API application.

Install the packages

The new .NET Core configuration components are published under Microsoft.Extensions.Configuration.* packages on NuGet. For this demo I’ve installed the following packages:

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Json (support for JSON configuration files)
  • Microsoft.Extensions.Configuration.Binder (strongly-typed binding of configuration settings)

Initialising configuration

To initialise the configuration system we use ConfigurationBuilder. When you install additional configuration sources the builder will be extended with a number of new methods for adding those sources. Finally call Build() to create a configuration instance:

IConfigurationRoot configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json.config", optional: true)
    .Build();

Accessing configuration settings

Once you have the configuration instance, settings can be accessed using their key:

var applicationName = configuration["ApplicationName"];

If your configuration settings have a heirarchical structure (likely if you’re using JSON or XML files) then each level in the heirarchy will be separated with a :.

To demonstrate I’ve added a appsettings.json.config file containing a few configuration settings:

{
  "connectionStrings": {
    "MyDb": "server=localhost;database=mydb;integrated security=true"
  },
  "apiSettings": {
    "url": "http://localhost/api",
    "apiKey": "sk_1234566",
    "useCache":  true
  }
}

Note: I’m using the .config extension as a simple way to prevent IIS serving these files directly. Alternatively you can set up IIS request filtering to prevent access to your JSON config files.

I’ve then wired up an endpoint in my controller to return the configuration, using keys to access my values:

public class ConfigurationController : ApiController
{
    public HttpResponseMessage Get()
    {
        var config = new
        {
            MyDbConnectionString = Startup.Config["ConnectionStrings:MyDb"],
            ApiSettings = new
            {
                Url = Startup.Config["ApiSettings:Url"],
                ApiKey = Startup.Config["ApiSettings:ApiKey"],
                UseCache = Startup.Config["ApiSettings:UseCache"],
            }
        };

        return Request.CreateResponse(config);
    }
}

When I hit my /configuration endpoint I get the following JSON response:

{
	"MyDbConnectionString": "server=localhost;database=mydb;integrated security=true",
	"ApiSettings": {
		"Url": "http://localhost/api",
		"ApiKey": "sk_1234566",
		"UseCache": "True"
	}
}

Strongly-typed configuration

Of course accessing settings in this way isn’t a vast improvement over using ConfigurationManager and as you’ll notice above, we’re not getting the correct type for all of our settings.

Fortunately the new .NET Core configuration system supports strongly-typed binding of your configuration, using Microsoft.Extensions.Configuration.Binder.

I created the following class to bind my configuration to:

public class AppConfig
{
    public ConnectionStringsConfig ConnectionStrings { get; set; }
    public ApiSettingsConfig ApiSettings { get; set; }

    public class ConnectionStringsConfig
    {
        public string MyDb { get; set; }
    }   

    public class ApiSettingsConfig
    {
        public string Url { get; set; }
        public string ApiKey { get; set; }
        public bool UseCache { get; set; }
    }
}

To bind to this class directly, use the Get<T> extensions provided by the binder package. Here’s my updated controller:

public HttpResponseMessage Get()
{
    var config = Startup.Config.Get<AppConfig>();
    return Request.CreateResponse(config);
}

The response:

{
   "ConnectionStrings":{
      "MyDb":"server=localhost;database=mydb;integrated security=true"
   },
   "ApiSettings":{
      "Url":"http://localhost/api",
      "ApiKey":"sk_1234566",
      "UseCache":true
   }
}

Now I can access my application configuration in a much nicer way:

if (config.ApiSettings.UseCache)
{

}

What about Web/App.config?

So far I’ve demonstrated how to use some of the new configuration features in a legacy application. But what if you still rely on traditional XML based configuration files like web.config or app.config?

In my application I still have a few settings in app.config (I’m self-hosting the API) that I require in my application. Ideally I’d like to use the .NET core configuration system to bind these to my AppConfig class too:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name="MyLegacyDb" connectionString="server=localhost;database=legacy" />
  </connectionStrings>
  <appSettings>
    <add key="ApplicationName" value="CoreConfigurationDemo"/>
  </appSettings>
</configuration>

There is an XML configuration source for .NET Core. However, if you try and use this for appSettings or connectionStrings elements you’ll find the generated keys are not really ideal for strongly typed binding:

IConfigurationRoot configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json.config", optional: true)
    .AddXmlFile("app.config")
    .Build();

If we inspect the configuration after calling Build() we get the following key/value for the MyLegacyDb connection string:

[connectionStrings:add:MyLegacyDb:connectionString, server=localhost;database=legacy]

This is due to how the XML source binds XML attributes.

Given that we still have access to the older System.Configuration system it makes sense to use this to access our XML config files and then plug the values into the new .NET core configuration system. We can do this by creating a custom configuration provider.

Creating a custom configuration provider.

To implement a custom configuration provider you implement the IConfigurationProvider and IConfigurationSource interfaces. You can also derive from the abstract class ConfigurationProvider which will save you writing some boilerplate code.

For a more advanced implementation that requires reading file contents and supports multiple files of the same type, check out Andrew Lock’s write-up on how to add a YAML configuration provider.

Since I’m relying on System.Configuration.ConfigurationManager to read app.config and do not need to support multiple files, my implementation is quite simple:

public class LegacyConfigurationProvider : ConfigurationProvider, IConfigurationSource
{
    public override void Load()
    {
        foreach (ConnectionStringSettings connectionString in ConfigurationManager.ConnectionStrings)
        {
            Data.Add($"ConnectionStrings:{connectionString.Name}", connectionString.ConnectionString);
        }

        foreach (var settingKey in ConfigurationManager.AppSettings.AllKeys)
        {
            Data.Add(settingKey, ConfigurationManager.AppSettings[settingKey]);
        }
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return this;
    }
}

When ConfigurationBuilder.Build() is called the Build method of each configured source is executed, which returns a IConfigurationProvider used to get the configuration data. Since we’re deriving from ConfigurationProvider we can override Load, adding each of the connection strings and application settings from app.config.

I updated my AppConfig to include the new setting and connection string as below:

public class AppConfig
{
    public string ApplicationName { get; set; }
    public ConnectionStringsConfig ConnectionStrings { get; set; }
    public ApiSettingsConfig ApiSettings { get; set; }

    public class ConnectionStringsConfig
    {
        public string MyDb { get; set; }
        public string MyLegacyDb { get; set; }
    }   

    public class ApiSettingsConfig
    {
        public string Url { get; set; }
        public string ApiKey { get; set; }
        public bool UseCache { get; set; }
    }
}

The only change I need to make to my application is to add the configuration provider to my configuration builder:

IConfigurationRoot configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json.config", optional: true)
    .Add(new LegacyConfigurationProvider())
    .Build();

Then, when I hit my /configuration endpoint I get my complete configuration, bound from both my JSON file and app.config:

{
   "ApplicationName":"CoreConfigurationDemo",
   "ConnectionStrings":{
      "MyDb":"server=localhost;integrated security=true",
      "MyLegacyDb":"server=localhost;database=legacy"
   },
   "ApiSettings":{
      "Url":"http://localhost/api",
      "ApiKey":"sk_1234566",
      "UseCache":true
   }
}

© 2022 Ben Foster