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.
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).DefaultCurrency
. We can’t know this by looking at the code, and the knowledge resides only in developers heads.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.
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
DefaultCurrency
is now explicit. It is not possible to create the class without it.DefaultCurrency
is satisfied through constructor injection.ICurrencyConversionConfiguration
when testing.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
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.
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.
Encapsulating configuration logic in this way, and providing configuration through an interface, has the following benefits.
rename
), can be used and guarantee that all instances are updatedIf you would like to use it, there is a nuget package available, and the source is on GitHub.