Teaching StructureMap About C# 4.0 Optional Parameters and Default Values

June 4, 2010
SEP hex pattern overlay of person working on laptop

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).

publicclassLogCommand

{

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 =newContainer(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:

publicclassDefaultCtorParameterConvention : IRegistrationConvention

{

publicvoid 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.

publicstaticbool HasOptionalParameters(thisConstructorInfo ctor)

{

return ctor.GetOptionalParameters().Any();

}

publicstaticIEnumerable<ParameterInfo> GetOptionalParameters(

thisConstructorInfo ctor)

{

return ctor.GetParameters().Where(

param => param.Attributes.HasFlag(ParameterAttributes.Optional));

}

publicstaticConstructorInfo GetGreediestCtor(thisType target)

{

return target.GetConstructors()

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

}

publicstatic T WithMax<T>(thisIEnumerable<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 =newContainer(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!

Build awesome things for fun.

Check out our current openings for your chance to make awesome things with creative, curious people.

Explore SEP Careers »

You Might Also Like