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!
Single File Split Buffers in Visual Studio!
Man, I’d searched for this feature time and time again. And finally found it here: http://www.kevinwilliampang.com/post/Visual-Studio-Split-Views.aspx
If you don’t want to follow the link…
Just double click that guy, or drag it downward, and you’ve split your file into two buffers. Awesomeness.
Autotest… in .NET
The first time I saw autotest (presented by Anthony), the idea of Continuous Testing captured me.
I live in a .NET world most of the time, and I know of no similar solution for .NET. It’s been awhile since that first time, and I’ve tinkered here and there trying to get something comparable, but usually come up short. That is until I found watchr.
Watchr gave me the file change detection capabilities I needed, and the extensibility to do whatever I want when a file has been detected as changed. This made it incredibly easy to hook up some autotest goodness in my .NET world.
You'll have to have ruby installed, and gems. Then, the very first thing you'll have to do is
gem install watchr --source=http://gemcutter.org
Here is my watchr script:
require 'autotest.rb' watch( '^.*UnitTest.*.cs$' ) do |match| run_test(match.to_s) end
This is basically just a regex that says to watch any *.cs files that also contain the string “UnitTest”, and when it finds a change in a file matching that description, call run_test with the matched file name.
So all the magic is in autotest.rb… lets check it out:
require 'rexml/document'
def build(test_project)
`msbuild /nologo #{test_project}`
end
def mstest(test_container, test_results_file, tests_to_run)
tests_to_run = ([""] << tests_to_run).flatten
File.delete(test_results_file) if File.exists?(test_results_file)
`mstest /nologo /resultsfile:#{test_results_file} /testcontainer:#{test_container} #{tests_to_run.join(" /test:")}`
test_results = process_mstest_results(test_results_file)
File.delete(test_results_file) if File.exists?(test_results_file)
return test_results
end
def process_mstest_results(results_file)
results = {}
File.open(results_file) do |file|
xml = REXML::Document.new(file)
results[:num_tests] = xml.get_elements("//UnitTestResult").length
failures = []
xml.elements.each("//UnitTestResult[@outcome='Failed']") do |e|
failure = {}
failure[:message] = e.elements["Output/ErrorInfo/Message"].get_text
stack = e.elements["Output/ErrorInfo/StackTrace"].get_text.value
stack_match = /^.*at (.*) in(.*):line (\d+)$/.match(stack)
failure[:stack] = stack_match[1] if stack_match
failure[:location] = stack_match[2] if stack_match
failure[:line] = stack_match[3] if stack_match
failure[:stack] = stack if !stack_match
failures << failure
end
results[:failures] = failures
end
return results
end
def show_results(results)
puts "#{results[:num_tests]} tests run (#{results[:failures].length} failures)"
results[:failures].each do |failure|
puts "---------------------------------------"
puts "Message: #{failure[:message]}"
puts "Location: #{failure[:location]}"
puts "Line: #{failure[:line]}"
puts "Stack Trace: #{failure[:stack]}"
end
end
def run_test(file_name)
test_container = ""
test_results_file = "result.trx"
test_project = ""
system("cls")
system("echo Detected change in:")
system("echo #{file_name}")
system("echo Building and Testing")
test_namespace = ''
test_class = ''
test_names = []
File.open(file_name, "r") do |f|
f.each do |line|
ns_match = /^namespace (.*)$/.match(line)
test_namespace = ns_match[1] if ns_match
class_match = /^\s*public class (.\w*).*$/.match(line)
test_class = class_match[1] if class_match
test_name_match = /^\s*public void (\w*).*$/.match(line)
test_names << test_name_match[1] if test_name_match
end
end
test_names = test_names.map { |n| "#{test_namespace}.#{test_class}.#{n}" }
build(test_project)
results = mstest(test_container, test_results_file, test_names)
show_results(results)
end
The key parts (I think) are the fact that I’m using MSTest to run my tests (this can easily be modified to run your framework of choice… note MSTest is not my choice
). The result parsing is also specific to the MSTest output format, but should be simple enough for any framework that can output XML. Also, I'm making some assumptions based on my project... we've got one unit test project, so I know I can run tests in a single DLL, and rebuilding only that project, I don't have to worry about choosing the correct project and output dll to build and run tests in.
To get the thing up and running, just run
watchr <path to watchr script>
Please, use/adapt/give feedback/whatever at will.
Go forth and autotest, .NET comrades!
2 amazazing productivity tools
I'm in the middle of writing a bunch of XML API documentation for a prototype I just built. I'm not really a fan of xml doc-comments, which is why I didn't do it in the first place, but the client wants API documentation, so this is definitely the best way to get it. The two aforementioned tools?
GhostDoc
GhostDoc basically infers the documentation from the name of the method and its parameter signature. Absolutely brilliant. Hook this baby up with a keyboard shortcut, and blam!, it just spits out documentation with a keystroke (which of course is easy to tweak once it's there). One of the awesome features is that for implemented methods of an interface, it'll use the exact documentation from the doc-comments from the interface file. Sweet.
Docu
Docu is sort of like the NDoc of old. I know that SandCastle exists, but this is so much simpler. It uses the Spark view engine/templating system, so that means the output is completely customizable. Right now it comes with a single template, that is heavily inspired by rdoc rather than something like MSDN style (though I'm certain an MSDN style template will be contributed to the project soon). The project is really young, but it is used already by FluentNHibernate (and was the reason for its inception, really). Here is the output for the FluentNH project: FluentNH API Docs.
