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.
You Might Also Like