In most projects, you're going to have some configuration. In .NET projects, it'll probably start in your app.config or web.config file.
However, if you love TDD, you'll likely have notice that all of the built in configuration classes are horribly un-testable. They all revolve around static references to System.Configuration.ConfigurationManager, and don't really have any interfaces, so in every project, you end up wrapping them into something like "IAppSettingsWrapper", in order to write tests.
After writing these wrappers several thousand times, and being inspired by the excellent "System.IO.Abstractions" package, we've put together a standardised set of wrappers around these core framework classes.
If you want
The this is for you.
Via NuGet:
PM> Install-Package System.Configuration.Abstractions
The simplest use case is to bind up IConfigurationManager
to System.Configuration.Abstractions.ConfigurationManager
in your DI container.
Alternatively, you can use System.Configuration.Abstractions.ConfigurationManager.Instance
- a property that'll new up a new instance of IConfigurationManager each time it's accessed.
If you want to directly switch out calls to System.Configuration.ConfigurationManager
in-place, to take advantage of the strongly typed extensions and IConfigurationInterceptors
you can replace calls to System.Configuration.ConfigurationManager
with calls to System.Configuration.Abstractions.ConfigurationManager.Instance
in-line, and your code should function identically.
Lastly, you can just new up an instance of System.Configuration.Abstractions.ConfigurationManager
anywhere, using its default constructor, and everything'll be just fine.
Examples:
// Usages
// You can use the singleton
var valString = ConfigurationManager.Instance.AppSettings["stringKey"];
var valInt = ConfigurationManager.Instance.AppSettings.AppSetting<int>("intKey");
// You can new up an instance
var configMgr = new ConfigurationManager();
var valString2 = configMgr.AppSettings["stringKey"];
var valInt2 = configMgr.AppSettings.AppSetting<int>("intKey");
// You can new up an instance with configuration values
var configMgr3 = new ConfigurationManager(new NameValueCollection {{"stringKey", "hello"}, {"intKey", "123"}});
var valString3 = configMgr3.AppSettings["stringKey"];
var valInt3 = configMgr3.AppSettings.AppSetting<int>("intKey");
// You can just switch out calls in place for backwards compatible behaviour
var old = System.Configuration.ConfigurationManager.AppSettings["stringKey"];
var @new = ConfigurationManager.Instance.AppSettings["stringKey"];
// You can wire up to your container
ninjectContainer.Bind<IConfigurationManager>().ToMethod(()=> return new ConfigurationManager());
The IAppSettingsExtended
interface, which our AppSettingsExtended
class implements, contains four new methods:
string AppSetting(string key, Func<string> whenKeyNotFoundInsteadOfThrowingDefaultException = null);
T AppSetting<T>(string key, Func<T> whenKeyNotFoundInsteadOfThrowingDefaultException = null);
T AppSettingConvert<T>(string key, Func<T> whenConversionFailsInsteadOfThrowingDefaultException = null);
T AppSettingSilent<T>(string key, Func<T> insteadOfThrowingDefaultException = null);
These strongly typed "AppSetting" helpers, will convert any primitive types that Convert.ChangeType
supports. The most obvious use case being int / bool / float / int? / bool? from their string representations - keeping alot of noisy conversions out of your code. You can also provide an optional Func
The following usage examples illustrate how to use these helpers:
// Before *******************************
var settingThatIsAnInteger = System.Configuration.ConfigurationManager.AppSettings["key"];
int someInt;
if (Int32.TryParse(settingThatIsAnInteger, out someInt))
{
someInt = 123; // Default
}
var settingThatIsABool = System.Configuration.ConfigurationManager.AppSettings["otherKey"];
bool someBool;
if (bool.TryParse(settingThatIsAnInteger, out someBool))
{
someBool = true; // Default
}
bool? someBool;
try
{
var settingThatIsABool = System.Configuration.ConfigurationManager.AppSettings["otherKey"];
if (bool.TryParse(settingThatIsAnInteger, out someBool))
{
someBool = true; // Default
}
}
catch
{
someBool = true;
}
// After ********************************
var withADefault = ConfigurationManager.Instance.AppSettings.AppSetting("key", () => 123);
var withoutADefault = ConfigurationManager.Instance.AppSettings.AppSetting<int>("key");
var worksWithAllPrimatives = ConfigurationManager.Instance.AppSettings.AppSetting<bool>("otherKey");
var worksWithNullables = ConfigurationManager.Instance.AppSettings.AppSetting<bool?>("otherKey");
var customNotFoundHandler = ConfigurationManager.Instance.AppSettings.AppSetting<bool?>("otherKey", () =>
{
throw new MyCustomMissingKeyException();
});
var customNotFoundHandler = ConfigurationManager.Instance.AppSettings.AppSetting<bool?>("otherKey", () =>
{
throw new MyCustomMissingKeyException();
},
() =>
{
throw new MyCustomConversionException();
});
var customNotFoundHandler = configurationManagerExtended.AppSettingConvert<bool?>("otherKey", () =>
{
throw new MyCustomConversionException();
});
var defaultValue = true;
var silentHandler = configurationManagerExtended.AppSettingSilent<bool>("otherKey", () =>
{
// Log Warning
// ...
// Return Default
return defaultValue;
});
IConfigurationInterceptor
's are hooks that, if registered, allow you to intercept and manipulate the values retrieved from configuration.
IConnectionStringInterceptor
's are hooks that allow you to manipulate connection strings during retrieval in a similar manner.
To wire up an IConfigurationInterceptor
or IConnectionStringInterceptor
, first, implement one, then call the static method ConfigurationManager.RegisterInterceptors(interceptor);
Your interceptors are singletons and should be thread safe as the same instance could be called across multiple threads concurrently.
Example:
ConfigurationManager.RegisterInterceptors(new ConfigurationSubstitutionInterceptor());
var result = ConfigurationManager.Instance.AppSettings.AppSetting<string>("key"); // Interceptor executes
var result2 = ConfigurationManager.Instance.AppSettings["key"]; // Interceptor executes
Interceptors fire for both the AppSetting helper, and the standard NameValueCollection methods and indexers. If you want to by-pass interception, access the "Raw" property for the original collection. This is a change in behaviour in V2.
An obvious example would be the presence of an appSetting looking like this:
<add key="my-key" value="{machineName}-something" />
You could easily add an interceptor to detect and fill in {machineName}
from an environmental variable, keeping your configuration free of painful transformations.
There are several other useful scenarios (auditing and logging, substitution, multi-tenancy) that interceptors could be useful in.
The ConfigurationSubstitutionInterceptor
is bundled with the package, firstly as an example, but also as a useful configuration interceptor.
It supports embedding any appsetting into any other. Given:
<add key="key1" value="valueOfOne" />
<add key="key2" value="{key1}-valueOfTwo" />
With this interceptor registered, this is true:
var result = ConfigurationManager.AppSetting<string>("key2");
Console.WriteLine(result); // writes: valueOfOne-valueOfTwo
This interceptor will help you simplify transformed web or app config files that rely on similar / repetitive token replacements, by allowing you to override just one value, and have it nested across the rest of your configuration using the interceptor.
Wired up just like IConfigurationInterceptors
- TypeConverters let you specify custom type conversion logic.
We include one TypeConverter by default - that converters to Uri
. To implement your own type converter, you need to implement the following interface:
public interface IConvertType
{
Type TargetType { get; }
object Convert(string configurationValue);
}
and register your converter by using
var converter = new UserConverterExample();
ConfigurationManager.RegisterTypeConverters(converter);
Your type converter will then be invoked whenever you request a mapping to the type that your converter supports.
Send a pull request with a passing test for any bugs or interesting extension ideas.
David Whitney