paint-brush
Taming Configuration in C#by@cuddlyburger
9,642 reads
9,642 reads

Taming Configuration in C#

by cedd burgeNovember 14th, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

We have a lot of C# code that wants to access some kind of configuration, and this is all done through:
featured image - Taming Configuration in C#
cedd burge HackerNoon profile picture

The Problem

We have a lot of C# code that wants to access some kind of configuration, and this is all done through:

ConfigurationManager.AppSettings["SomeSettingOrOther"]

For example, we had a CurrencyConversion class, that had the following code.

public class CurrencyConversion{    Currency GetDefaultCurrency() {        // get the config setting        string configCurrency = ConfigurationManager.AppSettings["MarketCurrency"];

        // return the equivalent Enum        return CurrencyFromString(configCurrency);    }}

This code has several issues.

  • Its dependency on the MarketCurrency setting is implicit (ie we can’t know this by looking at the public interface of the class, we have to look in to the internals).
  • It can throw exceptions if the config setting is missing or badly formatted, and we will only find this out when running this particular bit of code.
  • There may be other parts of the code that also use this config setting, and that will repeat the conversion of the string to the Enum and any error handling.
  • Possibly it is using the wrong config setting (or reusing an existing setting for the sake of convenience), and should more properly be using DefaultCurrency. We can’t know this by looking at the code, and the knowledge resides only in developers heads.
  • Testing is difficult, as we have to set up the ConfigurationManager, and we probably have some trial and error whilst working out what configuration is required.
  • The configuration source (ConfigurationManager) is hard coded, and could vary over the codebase (that is with some methods using ConfigurationManager and some methods reading Environment Variables)

There were higher level problems too. These calls were spread throughout the code, sometimes deep in shared libraries, and it was impossible to know which bits of code needed which settings (without reading all of the code).

This meant it was difficult to validate that a config file contained all the information it needed, and that nobody dared to remove config settings. This in turn led to our config files to become increadingly bloated and confusing.

The Solution

To solve these problems we moved to using interfaces to define our configuration.

If we refactor the example code above to take its configuration via an interface we get the following (in real life, being as there is only one setting, you might decide just to pass it in directly, but bear with me).

public interface ICurrencyConversionConfiguration{    Currency DefaultCurrency;}

public class CurrencyConversion{    readonly ICurrencyConversionConfiguration configuration;

    public CurrencyConversion(ICurrencyConversionConfiguration configuration) {        Contract.Requires(configuration != null);

        this.configuration = configuration;    }

    Currency GetDefaultCurrency() {        return configuration.DefaultCurrency;    }}

This code has the following improvements

  • Its dependency on DefaultCurrency is now explicit. It is not possible to create the class without it.
  • The dependency on DefaultCurrency is satisfied through constructor injection.
  • It is easy to Mock ICurrencyConversionConfiguration when testing.
  • The name is consistent.
  • The responsibility for reading and parsing the configuration has been removed.

To handle the responsibility for reading and parsing the configuration we add the code below. This relies on a simple Configuration class, which you can see on GitHub at https://github.com/resgroup/configuration.

public class EconomicModelConfiguration : ICurrencyConversionConfiguration {    readonly Configuration configuration;        public EconomicModelConfiguration(Configuration configuration) {        Contract.Requires(configuration != null);

        this.configuration = configuration;            Validate();    }        void Validate() =>        using (var validator = configuration.CreateValidator)            validator.Check(() => DefaultCurrency);

    public string DefaultCurrency =>         configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());}

The configuration class itself is instantiated with a Configuration Source, which reads from environment variables in the example below. This makes it easy to adhere to 12 Factor App recommendations (https://12factor.net/config).

new Configuration(new GetFromEnvironment());

This has the following improvements

  • The config settings are checked when the class is created (which should be at the entry point of the application), so any config problems are flushed out immediately.
  • The code to convert from string to Currency is centralised.
  • The configuration source is encapsulated.

If we move another class to use the new configuration system, we get something like this.

public class EconomicModelConfiguration : ICurrencyConversionConfiguration, IConcreteCostConfiguration {    readonly Configuration configuration;        public EconomicModelConfiguration(Configuration configuration) {        Contract.Requires(configuration != null);

        this.configuration = configuration;            Validate();    }        void Validate() {        using (var validator = configuration.CreateValidator) {            validator.Check(() => DefaultCurrency);            validator.Check(() => DefaultConcreteCost);        }    }

    public string DefaultCurrency =>         configuration.GetEnum<Currency>(MethodBase.GetCurrentMethod());

    public double DefaultConcreteCost =>         configuration.GetDouble(MethodBase.GetCurrentMethod());}

This process continues as we move more classes over to the new system, and has the benefit that setting up Inversion of Control is easy, as we can just register EconomicModelConfiguration with all the interfaces that it implements.

Legacy Code

Like any mature software team, we have some legacy code, some of which is not ready to be created via Inversion of Control.

For these classes we create a static Configuation class

public static class EconomicModelConfigurationStatic{    readonly static EconomicModelConfiguration base = new EconomicModelConfiguration();

    public static IEconomicModelConfiguration Settings =>        base;}

In the Legacy code, we then replace

ConfigurationManager.AppSettings["SomeSettingOrOther"]

with

EconomicModelConfigurationStatic.Settings.SomeSettingOrOther

This gets us a lot of the benefits of the new system, with only very minor and easy changes to the existing code.

Conclusions

Encapsulating configuration logic in this way, and providing configuration through an interface, has the following benefits.

  • There are no magic strings, so any misspellings will be caught at compile time
  • Refactoring tools (eg rename), can be used and guarantee that all instances are updated
  • All references to an item of configuration can be easily found using Visual Studio
  • Code is explicit about the configuration it requires, and can define only the subset of the configuration that it needs
  • Configuration files can be checked to see if they contain all the required information
  • Configuration files can be checked to see if they contain any surplus information
  • Configuration logic, such as defaults and conversion is handled centrally
  • Configuration items are guaranteed to have the same name in the config file and in the code

If you would like to use it, there is a nuget package available, and the source is on GitHub.