private override

4Jun/103

Teaching StructureMap About C# 4.0 Optional Parameters and Default Values

This week I ran into wanting to use C# 4.0 optional parameters, but wanted StructureMap (my IoC tool of choice) to respect the default value specified for those optional parameters.

The Problem

In this example, we’ll be pulling a command out of the container.  The important part is the optional constructor parameter (level), and it’s default value (Level.Info).

public class LogCommand

{

    IDestination _destination;

    Level _level;

 

    public LogCommand(

        IDestination destination, Level level = Level.Info)

    {

        _destination = destination;

        _level = level;

    }

 

    /* logging code here */

}

 

Here is your basic usage, but doesn’t work since StructureMap doesn’t know how to take advantage of the optional parameters with default values.

var container = new Container(config =>

{

    config.Scan(scanner =>

    {

        scanner.TheCallingAssembly();

        scanner.AddAllTypesOf<IDestination>();

        scanner.WithDefaultConventions();

    });

});

 

var logCommand = container.GetInstance<LogCommand>();

 

The last line results in an exception because StructureMap doesn’t know how to fill in the level parameter.

The Solution

We can solve this by adding a new convention.  One that adds information about default constructor arguments.  Here is the implementation of the convention:

public class DefaultCtorParameterConvention : IRegistrationConvention

{

    public void Process(Type type, Registry registry)

    {

        if(type.IsAbstract || type.IsEnum)

            return;

 

        var ctor = type.GetGreediestCtor();

 

        if(!ctor.HasOptionalParameters())

            return;

 

        var inst = registry.For(type).Use(type);

 

        foreach(var param in ctor.GetOptionalParameters())

            inst.Child(param.Name).Is(param.DefaultValue);

    }

}

Note: GetGreediestCtor, HasOptionalParameters, and GetOptionalParameters are extension methods.  We’ll see their implementation shortly.

The convention inherits from the IRegistrationConvention, which is how you implement new conventions in StructureMap.  It has only one method: Process.  We filter out types that are abstract, are enums, or have constructors that don’t have optional parameters.  Once we realize we have a constructor we want to deal with, we use the Child method, that sets either a property or a constructor argument (for our case, it’ll always be a constructor argument), and then we set it’s value to the parameter’s default value, as provided by the ParameterInfo object, for each optional parameter.

Minor Details

Curious about the implementation of GetGreediestCtor or the *OptionalParameters methods?  If not, skip this section.

public static bool HasOptionalParameters(

    this ConstructorInfo ctor)

{

    return ctor.GetOptionalParameters().Any();

}

 

public static IEnumerable<ParameterInfo> GetOptionalParameters(this ConstructorInfo ctor)

{

    return ctor.GetParameters().Where(

        param => param.Attributes

            .HasFlag(ParameterAttributes.Optional));

}

 

public static ConstructorInfo GetGreediestCtor(

    this Type target)

{

    return target.GetConstructors()

        .WithMax(ctor => ctor.GetParameters().Length);

}

 

public static T WithMax<T>(

    this IEnumerable<T> target, Func<T, int> selector)

{

    int max = -1;

    T currentMax = default(T);

 

    foreach(var item in target)

    {

        var current = selector(item);

        if(current <= max)

            continue;

 

        max = current;

        currentMax = item;

    }

 

    return currentMax;

}

 

 

The Usage

Here’s how to use your new convention.

var container = new Container(config =>

{

   config.Scan(scanner =>

   {

       scanner.TheCallingAssembly();

       scanner.AddAllTypesOf<IDestination>();

       scanner.WithDefaultConventions();

       scanner.Convention<DefaultCtorParameterConvention>();

   });

});

 

var logCommand = container.GetInstance<LogCommand>();

 

Now, when we pull the LogCommand out of the container, the level parameter gets defaulted to Level.Info, just like we specified in the constructor.  Sweet!

Conclusion

This implementation is somewhat limiting, but the version I have in my github repo is a little more open and configurable.  It allows you to customize the instance key/name you use when registering your type, and also allows you to do additional, non-standard registrations if you need to.

Also, this doesn’t work if you’ve selected a constructor using the SelectConstructor config API from StructureMap, I’m not sure how to tap into that facility to look for that constructor rather than the greediest.

Am I missing something?  Did something not make sense?  Leave me a note!

Comments (3) Trackbacks (0)
  1. Thanks a lot for the code, solved one of the issues I was having! Kudoz! Let’s hope a future release of StructureMap will include built-in support!

    Thanks again!

  2. Hello again,

    I had to adjust your code slightly to make sure it works for situations where the given type would have no constructors at all, since they are also passed to the convention:
    [code]
    public static IEnumerable GetOptionalParameters(this ConstructorInfo ctor)
    {
    return ctor == null ? Enumerable.Empty() : ctor.GetParameters().Where(param => param.Attributes.HasFlag(ParameterAttributes.Optional));
    }
    [/code]
    This will ensure that the Any() call won’t throw an error and you don’t have to adjust the convention code itself.

    I also had to adjust your convention a bit for when a given type implements an interface yet has optional constructor parameters:
    [code]
    var interfaceName = type.GetInterfaces().FirstOrDefault();
    var interfaceType = interfaceName == null ? type : type.GetInterface(interfaceName.Name) ?? type;
    var inst = registry.For(interfaceType).Use(type);
    [/code]
    I realize grabbing the first implemented interface is a code smell but you have no reliable way of telling if an implemented interface is configured or going to be configured with StructureMap. So this way if no interface is found the type itself will be used for the registration; otherwise the found interface type is used for registration.

    Thanks again for pushing me into the direction needed on this one!

  3. Bleh, this comment shizzle completely destroys code snippets containing generic tags, I have no clue howto fix that with some special tag or something sorry :-(


Leave a comment


No trackbacks yet.