A Simple Trick to Centralize Your .NET Configuration
Written by Corbin March
Is your .NET configuration a mess? Do you wish there was a way to centralize configuration instead of deploying config files with every application instance? Have you considered rolling your own configuration handling, but were too nervous to stray from .NET convention? Here’s a useful hack: intercept requests for configuration data in a ProtectedConfigurationProvider and store the data wherever and however you choose.
Why?
Configuration isn’t sexy. Ask 100 developers what they’d like to work on and you could get 100 different answers, but none of them will be “configuration.” Because of that, configuration doesn’t get a lot of love. It tends to become brittle as applications grow. For each new configuration-driven feature, deployment environment, layer, or key naming convention, the chance of a configuration-driven bug grows.
As problems materialize, developers might be tempted to try a new configuration library, like Nini, or write their own configuration handling from scratch. The drawback to these approaches is that they break from .NET convention. You’re no longer able to fetch configuration using ConfigurationManager.Section[“Key”]. This could be fine in isolation, but breaks down when you reference an external assembly or someone references yours. If you reference an assembly that requires .NET configuration, you have to maintain two configuration sets and use two methods for retrieval. If you share your assembly with other teams, a dependency on an external configuration library can be a real drag.
Ideally, a flexible configuration handler would allow us to:
- Use .NET convention
- Create arbitrary structure in configuration data
- Store configuration data in a centralized location
- Store configuration data in a relational database, as JSON in Mongo, or in any other form we choose
- Protect our stored data with whichever encryption scheme we choose
Fortunately, the .NET Provider Model offers sensible extension points. The ProtectedConfigurationProvider wasn’t designed to handle all of our requirements, but with a little imagination, it does.
ProtectedConfigurationProvider
The ProtectedConfigurationProvider is an abstract class that makes it easy to protect (encrypt and decrypt) configuration data. .NET ships with two concrete implementations: DpapiProtectedConfigurationProvider and RsaProtectedConfigurationProvider. The thought was, if neither provider suits you, you can easily provide your own encryption implementation by extending ProtectedConfigurationProvider.
Lucky for us, a ProtectedConfigurationProvider isn’t limited to encryption and decryption. Its contract only requires we return decrypted configuration data in an XmlNode when presented with an encrypted XmlNode. For our purposes, we can ignore the encrypted node and return our configuration data after fetching it and transforming it as we see fit.
Implementation
I’ll work backward and describe what the consuming application needs first. We specify two things in our app’s config file. First, we tell our app that we’re using a custom configuration provider. Second, we indicate which parts of our config are “protected” by our provider. In this case, my provider handles appSettings.
<!--?xml version="1.0" encoding="utf-8" ?-->
<configuration><!--This registers our new provider with the runtime.-->
<configprotecteddata defaultprovider=" CustomProtectedConfigProvider ">
<providers>
<add name="CustomProtectedConfigProvider" settingformyprovider1="important value"
settingformyprovider2="”256”" type="CustomConfig.CustomProtectedConfigProvider, CustomConfig">
</add></providers>
</configprotecteddata><!--This tells the runtime to use our provider to “protect” appSettings.-->
<appsettings configprotectionprovider="CustomProtectedConfigProvider">
<!--This is required. Described below.-->
<encrypteddata></encrypteddata>
</appsettings></configuration>
If you’ve registered a Provider or HttpModule before, this should look familiar. There are two interesting bits to note. First, the configProtectedData/providers/add node can contain zero to many attributes beyond name and type. These attributes and values are passed to our custom provider during initialization. Next, our appSettings node must contain an EncryptedData element. It’s required. The configuration API will throw an exception without it. Typically, this element would contain cypher text from our provider. In our case, we’re going to ignore the element and pull configuration from somewhere else.
Moving to our provider, we focus on three methods. The contract requires that we implement Encrypt and Decrypt. We can safely ignore the Encrypt method (throw a NotImplementedException), since our implementation only retrieves data. An encryption-based ProtectedConfigurationProvider needs the Encrypt method to generate cypher text from plain text configuration XML. If we want the attributes from our registration XML, we’ll also want to override Initialize.
public class CustomProtectedConfigProvider : ProtectedConfigurationProvider
{
public override void Initialize(string name, NameValueCollection config)
{
// These attributes and values come from our registration XML above.
Setting1 = config["settingForMyProvider1"];
Setting2 = config["settingForMyProvider2"];base.Initialize(name, config);
}public override XmlNode Decrypt(XmlNode encryptedNode)
{
// We'll flesh this out below.
throw new NotImplementedException();
}public override XmlNode Encrypt(XmlNode node)
{
throw new NotImplementedException();
}
}
The real work happens in Decrypt. It’s here that we fetch our data from where it’s stored and construct the appSettings XML. Notice that we return the entire appSettings element with its children. It’s tempting to simply replace the EncryptedData node, but that’s not what the API expects.
public override XmlNode Decrypt(XmlNode encryptedNode)
{
var buffer = new StringBuilder(512);
buffer.Append("<appsettings>");
foreach (var setting in GetConfigDataFromExternalSource())
{
buffer.Append("<add key="\"").Append(setting.Key).Append("\"" value="\"").Append(setting.Value).Append("\"">");
}
buffer.Append("</add></appsettings>");var doc = new XmlDocument();
doc.LoadXml(buffer.ToString());return doc.DocumentElement;
}
A Few More Details
The sample code is intentionally simple. Obviously, there are a lot of details behind GetConfigDataFromExternalSource to consider. I won’t cover them here. Everyone’s needs are unique. Use your imagination. Think big. Store your configuration data in the best place and format for your project.
As written, the CustomProtectedConfigProvider knows nothing about which configuration section is being requested, which app is requesting the data, or where the app is running. It can only return appSettings from a single source. This won’t be useful for long. To make it smarter, it’s useful to pass some context along with the encrypted XmlNode. You can do this by adding attributes or including child elements in the EncryptedData node.
<encrypteddata>
<context><section>appSettings</section>
<environment>QA<!-- Environment-->
</environment></context>
</encrypteddata>
With this in place, the provider can determine context during the Decrypt process and respond accordingly.
I first stumbled on the idea of intercepting configuration requests using a ProtectedConfigurationProvider in the article Redirecting Configuration with a Custom Provider. MSDN provides a few examples of more conventional custom providers including a Triple DES implementation.
If you’d like more details on configuring your provider, check out Specifying a Protected Configuration Provider.
Image courtesy of Unsplash.