Saturday, January 18, 2014

Sharing Generic Configuration Across Projects

When you work with a team of developers on a large application across multiple projects, everyone is going to need access to unique configuration settings. Often this will give rise to each project having it's own separate configuration services, or worse everyone will just call the static ConfiguationManager directly.

How do you share a single configuration object across projects?

The answer is simple: extension methods! Just create a generic configuration service in your core library, and then have all of your other projects call it via extension methods. This ensures that you only have one testable, injectable, and extensible source for configuration. Additionally it keeps all of your configuration both discoverable and abstracted to their consuming project.

public interface IConfigService
{
    T Get<T>(string key);
    T Get<T>(string key, T defaultValue);
}
 
public static class ConfigServiceExtensions
{
    private const string NotificationsKey = "AreNotificationsEnabled";
    private const bool NotificationsDefault = true;
 
    public static bool AreNotificationsEnabled(this IConfigService config)
    {
        return config.Get(NotificationsKey, NotificationsDefault);
    }
}

Remember, every project can have it's own set of ConfigService extension methods. :)

Implementation

Here is a simple implementation of the IConfigService for System.Configuration.ConfigurationManager.

public class ConfigService : IConfigService
{
    private const string NotFoundMsg
        = "Required Configuration Value Not Found: ";
 
    private const string InvalidValueMsg
        = "Unable To Parse Configuration Value: ";
 
    private readonly ConcurrentDictionary<string, dynamic> _map
        = new ConcurrentDictionary<string, dynamic>();
 
    public T Get<T>(string key)
    {
        return Get(key, false, default(T));
    }
 
    public T Get<T>(string key, T defaultValue)
    {
        return Get(key, true, defaultValue);
    }
 
    private T Get<T>(string key, bool hasDefaultValue, T defaultValue)
    {
        return _map.GetOrAdd(key, k =>
        {
            T value;
            var result = Get(k, out value);
 
            switch (result)
            {
                case ResultType.NotFound:
                    if (hasDefaultValue)
                        return defaultValue;
 
                    throw new ConfigurationErrorsException(
                        NotFoundMsg + key);
 
                case ResultType.InvalidValue:
                    throw new ConfigurationErrorsException(
                        InvalidValueMsg + key);
 
                default:
                    return value;
            }
        });
    }
 
    private ResultType Get<T>(string key, out T value)
    {
        var stringValue = GetValue(key);
        if (String.IsNullOrWhiteSpace(stringValue))
        {
            value = default(T);
            return ResultType.NotFound;
        }
 
        try
        {
            var type = typeof (T);
            var converter = TypeDescriptor.GetConverter(type);
            value = (T) converter.ConvertFrom(stringValue);
            return ResultType.Success;
        }
        catch
        {
            value = default(T);
            return ResultType.InvalidValue;
        }
    }
 
    protected virtual string GetValue(string key)
    {
        return ConfigurationManager.AppSettings[key];
    }
 
    private enum ResultType
    {
        Success,
        InvalidValue,
        NotFound
    }
}

So that is the implementation...

Unit Tests

...and as always, here are some tests!

public static class ConfigServiceExtensions
{
    private const string IsValidKey = "IsValid";
    public static bool IsValid(this IConfigService config)
    {
        return config.Get<bool>(IsValidKey);
    }
 
    private const string IsDefaultKey = "IsDefault";
    private const bool IsDefaultValue = true;
    public static bool IsDefault(this IConfigService config)
    {
        return config.Get(IsDefaultKey, IsDefaultValue);
    }
 
    private const string IsInvalidKey = "IsInvalid";
    public static bool IsInvalid(this IConfigService config)
    {
        return config.Get(IsInvalidKey, false);
    }
 
    private const string IsNotFoundKey = "IsNotFound";
    public static bool IsNotFound(this IConfigService config)
    {
        return config.Get<bool>(IsNotFoundKey);
    }
}
 
public class ConfigTests
{
    public class TestConfigService : ConfigService
    {
        private static readonly ConcurrentDictionary<string, dynamic> Map;
 
        static TestConfigService()
        {
            Map = new ConcurrentDictionary<string, dynamic>();
            Map["IsValid"] = "false";
            Map["IsInvalid"] = "meh";
        }
 
        protected override string GetValue(string key)
        {
            return Map.ContainsKey(key)
                ? Map[key]
                : String.Empty;
        }
    }
 
    [Fact]
    public void IsDefault()
    {
        IConfigService config = new TestConfigService();
        var value = config.IsDefault();
        Assert.Equal(true, value);
    }
 
    [Fact]
    public void IsInvalid()
    {
        IConfigService config = new TestConfigService();
        Assert.Throws<ConfigurationErrorsException>(() =>
        {
            config.IsInvalid();
        });
    }
 
    [Fact]
    public void IsNotFound()
    {
        IConfigService config = new TestConfigService();
        Assert.Throws<ConfigurationErrorsException>(() =>
        {
            config.IsNotFound();
        });
    }
 
    [Fact]
    public void IsValid()
    {
        IConfigService config = new TestConfigService();
        var value = config.IsValid();
        Assert.Equal(false, value);
    }
}
Shout it

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics