I've been thinking and working with application configuration in ASP.NET applications for years, and it's become a tool that I'm very comfortable using. I can add AppSettings
, create configuration sections, and manage connectionstrings without thinking twice. However, there is a problem with the current ConfigurationManager
and the XML-based config file offering in the .NET Framework: how do I get configuration entries from other sources into my application so that I don’t need to build my own configuration client and tools? I just want to continue using the standard syntax to access appsettings like this:
var serviceId = ConfigurationManager.AppSettings["ServiceID"];
Introducing ConfigurationBuilders
Web.config and app.config have always been able to read external XML files, and assimilate their contents into .NET Configuration. In order to get application configuration from more sources, the ASP.NET team added the ConfigurationBuilder
feature. The idea is simple: you add notation to your web.config or app.config for the ConfigurationBuildersSection
and then you can load external configuration builders that will populate or modify the contents of a designated section. Let’s take a look at updating an application to get configuration from environment variables. Our standard configuration, stored in a web.config file, would look something like this:
<configuration>
<appSettings>
<add key="ServiceID" value="JeffsService" />
<add key="ServiceKey" value="TopSecret" />
</appSettings>
<connectionStrings>
<add name="default" connectionString="Data Source=mydb.db" />
</connectionStrings>
...
</configuration>
To illustrate these configuration values, I wrote a simple application that output the values of appsettings
to a web page. That page looks like this:
If we wanted to replace the appsettings
for the ServiceID
and ServiceKey
, as well as point the defaultConnectionString
at a new value provided by an environment variable, I can introduce a ConfigurationBuilder
that will read the environment variables and replace these values in the ConfigurationManager
that provides configuration data to my application. We can introduce the configuration builder by adding some simple markup to our configuration file:
<configuration>
<configSections>
<section name="configBuilders"
type="System.Configuration.ConfigurationBuildersSection,
System.Configuration, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
</configSections>
<configBuilders>
<builders>
<add name="Env"
type="MyConfigBuilders.EnvironmentConfigBuilder, MyConfigBuilders" />
</builders>
</configBuilders>
<appSettings configBuilders="Env">
<add key="ServiceID" value="JeffsService" />
<add key="ServiceKey" value="TopSecret" />
</appSettings>
<connectionStrings configBuilders="Env">
<add name="default" connectionString="Data Source=mydb.db" />
</connectionStrings>
...
</configuration>
The introduction of the configBuilders
section with an entry for the configuration builder we want to use is simple markup that can be added to any configuration file. The catch is adding the configBuilders
attribute to the sections that you want to apply the config builder to. In this case, we added it to the appSettings
and connectionStrings
elements. When the ConfigurationManager
is first used and parses this file, it will load the configurationbuilder
named Env
in the configBuilders
section and hand these sections to it for parsing. The Env
entry points to a class called EnvironmentConfigBuilder
whose source looks like:
public class EnvironmentConfigBuilder : ConfigurationBuilder
{
private readonly IDictionary _EnvVars;
public EnvironmentConfigBuilder()
{
_EnvVars = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process);
if (_EnvVars.Count == 0) _EnvVars = Environment.GetEnvironmentVariables();
Debug.WriteLine(_EnvVars.Count);
}
public override XmlNode ProcessRawXml(XmlNode rawXml)
{
foreach (DictionaryEntry envVar in _EnvVars)
{
var pair = (Key: envVar.Key.ToString(), Value: envVar.Value.ToString());
if (rawXml.HasChildNodes
&& rawXml.SelectSingleNode($"add[@key='{pair.Key}']") != null)
{
rawXml.SelectSingleNode($"add[@key='{pair.Key}']")
.Attributes["value"].Value = pair.Value;
}
}
return rawXml;
}
public override ConfigurationSection ProcessConfigurationSection(
ConfigurationSection configSection)
{
return base.ProcessConfigurationSection(configSection);
}
}
}
That’s a pretty simple class that processes the XML handed to it, and replaces the values with any matching environment variable names. In a sample application that reports its appsettings
values on a web page, I would see the following with appropriate values set in my production space:
Settings Overridden from Environment Variables in Production
Usage with Docker
This means that we can use this same application with the configuration builders attached and push settings into our container from outside. If we build this application with the Docker for Windows image microsoft/aspnet:4.7.1, we can then inject settings from outside the instance of the container. I’ll run my container with a command like the following:
docker run -d -p 80:80 -e ServiceID=Docker -e ServiceKey=DockerKey myapp
... and if I browse to my running container on Windows, I’ll now see the following:
Summary
With no code changes, we’re able to inject settings into our application from an outside source that isn’t just another file. You can write configuration builders that read configuration from any source, and just update your web.config to consume that new configuration builder. In the weeks ahead, look for an official set of configuration builders from the ASP.NET team that supports JSON files, environment variables, and a number of other sources. I’ve even heard a request to write a configuration builder to read from an INI file.
Stay tuned, as we make using your applications in containers and on cloud services even easier.