Sunday, February 07, 2010

10 Advanced Windsor Tricks – 10. Configuration with type converters

Here’s part ten of (at least) 10 Advanced Windsor Tricks.

This trick follows on nicely from trick number 9, ‘Configuring fluently registered components with XML’. Sometimes you might want a configuration value other than a simple string or integer. Windsor provides the ITypeConverter interface that you can implement to supply complex configuration for your components.

As an example, let’s imagine we have a class, HealthMonitor, that pings a series to endpoints and checks that a particular string is returned before a timeout. We want to configure HealthMonitor with an array of HealthEnpoints that specify a URL, an expected string value and a timeout in seconds. Something like this:

public interface IHealthMonitor
{
    HealthEndpoint[] HealthEndpoints { get; }
}

public class HealthMonitor : IHealthMonitor
{
    public HealthEndpoint[] HealthEndpoints { get; private set; }

    public HealthMonitor(HealthEndpoint[] healthEndpoints)
    {
        HealthEndpoints = healthEndpoints;
    }
}

public class HealthEndpoint
{
    public string Url { get; private set; }
    public string Expect { get; private set; }
    public int TimeoutSeconds { get; private set; }

    public HealthEndpoint(string url, string expect, int timeoutSeconds)
    {
        Url = url;
        Expect = expect;
        TimeoutSeconds = timeoutSeconds;
    }
}

I’ve left any implementation details out of HealthMonitor, it simply takes an array of HealthEndpoints that we can then inspect. We want our XML configuration to look something like this:

<component id="healthMonitor">
  <parameters>
    <healthEndpoints>
      <array>
        <item>
          <url>http://suteki.co.uk/ping</url>
          <expect>I am OK</expect>
          <timeoutSeconds>5</timeoutSeconds>
        </item>
        <item>
          <url>http://sutekishop.co.uk/ping</url>
          <expect>I am OK</expect>
          <timeoutSeconds>5</timeoutSeconds>
        </item>
        <item>
          <url>http://www.google.com</url>
          <expect>&lt;!doctype html&gt;</expect>
          <timeoutSeconds>1</timeoutSeconds>
        </item>
      </array>
    </healthEndpoints>
  </parameters>
</component>

Note that we want to use the standard Windsor configuration to say that the ‘healthEndpoints’ constructor parameter is made up of an array and each element of that array is described by ‘item’. We get that without doing anything special.

However, we do need some way of telling Windsor that when a component has a HealthMonitor dependency it should read a ‘url’, ‘expect’ and ‘timeoutSeconds’ value from the configuration and then construct a HealthEndpoint. We do that with a type converter.

Here’s our HealthEndpoint type converter:

public class HealthEndpointTypeConverter : AbstractTypeConverter
{
    public override bool CanHandleType(Type type)
    {
        return type == typeof (HealthEndpoint);
    }

    public override object PerformConversion(string value, Type targetType)
    {
        throw new NotImplementedException();
    }

    public override object PerformConversion(IConfiguration configuration, Type targetType)
    {
        var converter = new Converter(configuration.Children, Context);
        var url = converter.Get<string>("url");
        var expect = converter.Get<string>("expect");
        var timeoutSeconds = converter.Get<int>("timeoutSeconds");

        return new HealthEndpoint(url, expect, timeoutSeconds);
    }

    private class Converter
    {
        private readonly ConfigurationCollection configurationCollection;
        private readonly ITypeConverterContext context;

        public Converter(ConfigurationCollection configurationCollection, ITypeConverterContext context)
        {
            this.configurationCollection = configurationCollection;
            this.context = context;
        }

        public T Get<T>(string paramter)
        {
            var configuration =  configurationCollection.SingleOrDefault(c => c.Name == paramter);
            if (configuration == null)
            {
                throw new ApplicationException(string.Format(
                    "In the castle configuration, type '{0}' expects parameter '{1}' that was missing.",
                    typeof(T).Name, paramter));
            }
            return (T) context.Composition.PerformConversion(configuration, typeof (T));
        }
    }
}

We implement AbstractTypeConverter which provides us with some plumbing and implement three methods: ‘CanHandleType’ which tells Windsor which type(s) we are interested in; and two overloads of ‘PerformConversion’, one that takes a string and a target type and one that takes the current configuration section and a target type. Since we want to interpret the current configuration of each ‘item’ we’ll implement the second ‘PerformConversion’ method.

I’ve got a simple internal class ‘Converter’ that keeps track of child nodes of the ‘item’ node and the current context. It simply looks up each required property and converts it to a target type. Once we have our ‘url’, ‘expect’ and ‘timeoutSeconds’ values we construct and return a new HealthEndpoint. Note that we use Windsor’s built-in type converters for the simple target types: ‘context.Composition.PerformConversion’.

Wiring the type converter with Windsor is not obvious. You have to grab Windsor’s ConversionManager sub-system and add the new type converter to that. You could wrap that step with a facility, but there’s also a nice extension point, IWindsorInstaller, that you can also use to wrap up complex setup pieces:

public class HealthMonitorTypeConveterInstaller : IWindsorInstaller
{
    public void Install(IWindsorContainer container, IConfigurationStore store)
    {
        var manager = (IConversionManager)container.Kernel.GetSubSystem(SubSystemConstants.ConversionManagerKey);
        manager.Add(new HealthEndpointTypeConverter());
    }
}

Now we can bring it all together; installing our type converter and registering our HealthMonitor:

var container = new WindsorContainer()
    .Install(new HealthMonitorTypeConveterInstaller())
    .Install(Configuration.FromXmlFile("windsor.config"))
    .Register(
        Component.For<IHealthMonitor>().ImplementedBy<HealthMonitor>().Named("healthMonitor")
    );

If we resolve an IHealthMonitor and write out its enpoints like this:

var healthMonitor = container.Resolve<IHealthMonitor>();

foreach (var healthEndpoint in healthMonitor.HealthEndpoints)
{
    Console.Out.WriteLine("healthEndpoint.Url = {0}", healthEndpoint.Url);
    Console.Out.WriteLine("healthEndpoint.Expect = {0}", healthEndpoint.Expect);
    Console.Out.WriteLine("healthEndpoint.TimeoutSeconds = {0}", healthEndpoint.TimeoutSeconds);
}

We get this output on the console:

healthEndpoint.Url = http://suteki.co.uk/ping
healthEndpoint.Expect = I am OK
healthEndpoint.TimeoutSeconds = 5
healthEndpoint.Url = http://sutekishop.co.uk/ping
healthEndpoint.Expect = I am OK
healthEndpoint.TimeoutSeconds = 5
healthEndpoint.Url = http://www.google.com
healthEndpoint.Expect = <!doctype html>
healthEndpoint.TimeoutSeconds = 1

This is pretty simple example, but it’s possible to use type converters for some sophisticated configuration setups. Say you wanted to take some string value and parse it into a configuration class or maybe provide some kind of polymorphic configuration, all this can be done with a type converter.

OK, so that was trick 10, but don’t go away I’ve still got plenty more in the bag. Perhaps I should have used Scott Hanselman’s ‘… of an infinite series’ formula, but I quite like the fact that from now I’ll be over-delivering :)

5 comments:

Unknown said...

Don't stop at the top, as someone once sang.

This is an awesome series and keep it coming.

Mike Hadlow said...

Thanks Krzysztof. I'm going to have to start showing some off some the awesome stuff you've been adding to Windsor recently :)

Coryt said...

Took a while to figure this one out, but it looks like the 2.5.2+ release of Windsor needs a [Convertible] on the HealthEndpoint class.

atx said...

Actually you can get the HealthEndpoints property initialized without writing and registering custom type converter at all - just mark HealthEndpoint with [Convertible] attribute. The built-in ArrayConverter will nicely initialize your property even if it's an array of objects.

Tested on Windsor 3.2.1, so it may be just a new feature added after you wrote the post...

Anonymous said...

hello, just stopping by to say, "Thanks" for this. It was helpful to me.

Vince Zalamea