Config Builders

Photo by Scott Blake on Unsplash

I recently came across an addition to ASP.NET 4.7.1: configuration builders. With the .NET work I do, I generally have to maintain configuration across multiple environments, each with multiple sites. I was first introduced to config builders when investigating Azure KeyVault; however, even as an interim, using a JSON config file allows me to extract the environment-specific values and sensitive values from the web.config file for each site. (In the past it’s been a case of storing these directly on the server and/or using the config transforms – web.debug.config etc. – to translate values between environments. Neither is perfect, since if you miss a value or build for the wrong environment, all hell can break loose.)

Config builders solve this problem rather nicely: I have a config.json file (or several) in the app root in each environment, and use the config transforms for build differences (e.g. actual debug vs release values) and just read the requisite config file for each environment.

Lots of these values are either constant within an environment or for a particular site across environments, so breaking the config files down in this way means that separate environment-specific files can be symlinked, and site-specific is maintained for each site. (Sadly, but also wisely, you can’t go up from the app root to read config files…)

The issue I hit was with the numerous custom configuration sections in the web.config file; while the default config builders will allow for basically anything of the format

<add key="myKey" value="myValue"/>

(with the notable addition of connection strings), most of these custom configuration sections use attributes to define values. So how do we handle these? Turns out, they already thought of it .

So I created an abstract attribute-based handler like so:

public abstract class AttributeSectionHandlerBase<T> : SectionHandler<T> where T : ConfigurationSection
{
    public override IEnumerator<KeyValuePair<string, object>> GetEnumerator()
    {
        // appropriated from the appSettings handler:
        // https://github.com/aspnet/MicrosoftConfigurationBuilders/blob/main/src/Base/SectionHandler.cs#L97-103
        var properties = ConfigSection.GetType().GetProperties();
        foreach (var property in properties)
            yield return new KeyValuePair<string, object>(property.Name, property.GetValue(ConfigSection));
    }

    public override void InsertOrUpdate(string newKey, string newValue, string oldKey = null, object oldItem = null)
    {
        if (newValue == null) return;

        var properties = ConfigSection.GetType().GetProperties();
        // the @ prefix is just a convention to distinguish the values in the config.json
        var propertyName = newKey.Substring(1);

        foreach (var property in properties)
        {
            if (property.Name == propertyName)
            {
                property.SetValue(ConfigSection, newValue);
            }
        }
    }
}

Then we just need a concrete implementation for each custom section.

public MyCustomSectionHandler : AttributeSectionHandler<MyCustomSection>
{
    // literally nothing else to do here...
}

The only thing worth pointing out here is that I deliberately chose to prefix configuration which relied on attributes with @:

// config.json
{
  "MyCustomSection": {
    "@MyAttribute": "Some value"
  }
}

This is purely aesthetic so that I could easily distinguish between attribute- and element-based configuration. YMMV.