All the material I’ve seen talking about reading from .Net configuration files concentrates on explaining the API. The API of ConfigurationManager exposes members such as:
public static class ConfigurationManager { static ConfigurationManager(); public static NameValueCollection AppSettings { get; } public static ConnectionStringSettingsCollection ConnectionStrings { get; } public static object GetSection(string sectionName); public static void RefreshSection(string sectionName); public static Configuration OpenMachineConfiguration(); public static Configuration OpenMappedMachineConfiguration(ConfigurationFileMap fileMap); public static Configuration OpenExeConfiguration(ConfigurationUserLevel userLevel); public static Configuration OpenExeConfiguration(string exePath); public static Configuration OpenMappedExeConfiguration(ExeConfigurationFileMap fileMap, ConfigurationUserLevel userLevel); }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
That is a pretty fine-grained API. Every time a developer cracks open that class with intellisense, he is presented with too many choices. Besides that, after getting some functionality working (such as hiding an advanced search criteria based on configuration (for a particular application instance)), the code looks something like this, a jumble of user code and framework call:
public bool ShouldBeVisible(CriteriaName name) { string key = "criterion." + name + ".visible"; string value = ConfigurationManager.AppSettings.Get(key); if (string.IsNullOrEmpty(value)) { return true; } return Convert.ToBoolean(value); }
Given that this code wants to show the criterion by default and let configuration override that, this method needs to be tested. Because of the framework call to configuration, its more difficult to test than necessary. Note, if you aren’t writing automated tests for your code, you probably think I’m crazy right now.
The developer has to run the app three times to ensure this work:
- Once with no configuration setting, should default to true
- Once with a setting that explicity sets to true
- Once with a setting that explicity sets to false
In my experience as a developer and a manager, running the application manually to test these three posibilities takes longer than banging out the three short automated tests for these cases.
There is a problem, through. Because the call to ConfigurationManager (a static class) is embedded in the method, my test code needs to move or modify a REAL configuration file at test time.
The solution: wrap configuration and expose a course-grained interface to may application code. It’s very easy to write your own, but we tend to use the configuration interface defined in the Tarantino project. It’s definition is as follows:
public interface IConfigurationReader { string GetRequiredSetting(string key); int GetRequiredIntegerSetting(string key); bool GetRequiredBooleanSetting(string key); IEnumerable<string> GetStringArray(string key); bool? GetOptionalBooleanSetting(string key); string GetOptionalSetting(string key); }
This may be more than you care to use, so you can trim it down even more very easily. The resulting code is as follows:
public class UiPreferences : IUiPreferences { private readonly IConfigurationReader _reader; public UiPreferences(IConfigurationReader reader) { _reader = reader; } public bool ShouldBeVisible(CriteriaName name) { string key = "criterion." + name + ".visible"; string value = _reader.GetOptionalSetting(key); if (string.IsNullOrEmpty(value)) { return true; } return Convert.ToBoolean(value); } }
Because of the interface, our unit tests are very simple, and the code is simpler as well.
[Test] public void Should_show_criterion_by_default_without_configuration() { var settings = new NameValueCollection(); var configurationReader = new ConfigurationReader(new ConfigurationStub(settings)); var preferences = new UiPreferences(configurationReader); bool shouldBeVisible = preferences.ShouldBeVisible(CriteriaName.LastName); Assert.That(shouldBeVisible, Is.True); } [Test] public void Should_show_criterion_by_configuration() { var settings = new NameValueCollection {{"criterion.LastName.visible", "True"}}; var configurationReader = new ConfigurationReader(new ConfigurationStub(settings)); var preferences = new UiPreferences(configurationReader); bool shouldBeVisible = preferences.ShouldBeVisible(CriteriaName.LastName); Assert.That(shouldBeVisible, Is.True); } [Test] public void Should_hide_criterion_by_configuration() { var settings = new NameValueCollection {{"criterion.LastName.visible", "False"}}; var configurationReader = new ConfigurationReader(new ConfigurationStub(settings)); var preferences = new UiPreferences(configurationReader); bool shouldBeVisible = preferences.ShouldBeVisible(CriteriaName.LastName); Assert.That(shouldBeVisible, Is.False); }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }