Constructing settings classes that get read out of AppSettings on demand

My take on convention-based configuration for Autofac

I have an application that has external configuration. You probably do too. Here is my configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="Email:Port" value="2345"/>
    <add key="Email:Ssl" value="true"/>
    <add key="Email:Username" value="MyUserName" />
    <add key="Akismet:ApiKey" value="SomeKey" />
    <add key="Blog:EnableComments" value="true" />
    <add key="Blog:EnableHistory" value="true" />
  </appSettings>
</configuration>

As an Autofac user, I was already structuring my application with modules so I was really happy to see this post by Paul Stovell on Convention-based Configuration for Autofac. There were a few things I didn't like about it though.

Firstly, You are required to register modules in a weird way. Secondly, you need to specify the properties twice, once on the module and once as parameters in the module load method (and they need to be added to any constructors that need them too). I was sure Autofac could do better.

What I really wanted was to be able to create a setting class for each group of settings to be passed around and to be able to import that dependency through a constructor parameter into any class that needed it. Here is what it would look like:

class EmailSettings
{
    public string Port { get; set; }
    public bool UseSsl { get; set; }
    public string Username { get; set; }
}

class EmailServer
{
    private readonly EmailSettings _settings;
    public EmailServer(EmailSettings settings)
    {
        _settings = settings;
    }
}

The first task is to find a way to fill in EmailSettings from the AppSettings configuration node. Here is the class that does the job:

interface ISettingsReader
{
    object Load(Type type);
}

class SimpleSettingsReader : ISettingsReader
{
    private readonly NameValueCollection _settings;

    public SimpleSettingsReader(NameValueCollection settings)
    {
        _settings = settings;
    }

    public object Load(Type type)
    {
        if(type == null) throw new ArgumentNullException("type");
        var settingsObj = Activator.CreateInstance(type);
        var settingsPrefix = type.Name.Replace("Settings", "") + ":";
        foreach(var key in _settings.AllKeys.Where(x => x.StartsWith(settingsPrefix)))
        {
            var propertyName = key.Replace(settingsPrefix, "");
            var property = type.GetProperty(propertyName);
            if(property == null) 
                throw new Exception(String.Format("Settings class {0} has no property called {1}", type.Name, propertyName));
            var propertyValue = Convert.ChangeType(_settings[key], property.PropertyType);
            property.SetValue(settingsObj, propertyValue, null);
        }
        return settingsObj;
    }
}

It's not doing anything too complex. It creates an object of the settings type, checks the config file for matching settings and for each one it tries to find a corresponding property. This means that "Foo.Bar" will get mapped to the Bar property on the FooSettings class. Note that rather than rely on the actual configuration file, this class relies on NameValueCollection which makes it much easier to test. To use it with the configuration file I can register an instance wrapped around the config file app settings in my container builder like this:

var settingsReader = new SimpleSettingsReader(ConfigurationManager.AppSettings);
builder.RegisterInstance(settingsReader).As<ISettingsReader>();

And to use it in a class looks like this:

class EmailEngine
{
    private readonly EmailSettings _settings;

    public EmailEngine(ISettingsReader settingsReader)
    {
        _settings = (EmailSettings)settingsReader.Load(typeof(EmailSettings));
    }
}

Ugly. What I really want to be able to do is take a direct dependency on EmailSettings and have Autofac create the reader and read the settings for me at resolve time. As it turns out that is a great use for a custom registration source.

When Autofac is trying to resolve a Service it asks its internal collection of IRegistrationSource objects for advice on what to do. What we need to do is to create a registration source for settings classes and then tell our Autofac container about it. Here is the code for the registration source:

class SettingsSource : IRegistrationSource
{
    public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
    {
        var typedService = service as IServiceWithType;
        if (typedService != null && typedService.ServiceType.IsClass && typedService.ServiceType.Name.EndsWith("Settings"))
        {
            yield return RegistrationBuilder.ForDelegate(
                (c, p) => c.Resolve<ISettingsReader>().Load(typedService.ServiceType)
            ).As(typedService.ServiceType)
            .CreateRegistration();
        }
    }

    public bool IsAdapterForIndividualComponents
    {
        get { return false; }
    }
}

This class only has one method in it that can be roughly translated as "If you're looking for a type whose name ends with Settings, resolve an ISettingsReader and ask it to do the work". Finally we need to register the source so that our container will have access to it. I'm doing that in a module (along with the registration for the reader):

class SettingsModule : Module
{
    private readonly NameValueCollection _settings;

    public SettingsModule(NameValueCollection settings)
    {
        _settings = settings;
    }

    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<SimpleSettingsReader>()
            .As<ISettingsReader>()
            .WithParameter(TypedParameter.From(_settings));

        builder.RegisterSource(new SettingsSource());
    }
}

Now we can register the module:

builder.RegisterModule(new SettingsModule(ConfigurationManager.AppSettings));

And now the EmailEngine can take a direct dependency on the EmailSettings class:

class EmailEngine
{
    private readonly EmailSettings _settings;

    public EmailEngine(EmailSettings settings)
    {
        _settings = settings;
    }
}

Awesome! To add new settings to our application we just need a class whose name ends in Settings and we can start adding it to constructors and autowired properties right away.

P.S. As the properties on the settings classes are being set by reflection they MUST have a setter but that setter does not need to be public so I'd make them private.

Posted by: Mike Minutillo
Last revised: 22 Jun, 2011 06:09 AM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.