Or “How NOT to Use .NET Linq Expressions in Order to Get Parameter Name and Argument Value From C# Lambda?”
Yesterday I’ve managed to get overhead of 3-4 seconds per 1 million operations for the methods that did allowed to pass parameter name and argument value in one C# statement:
Enforce.Argument(() => args);
public static void Argument<K>(Expression<Func<K>> argument)
where K : class
{
var param = ParamCache<K>.Resolve(argument);
if (null == param.GetValue())
throw ArgumentNull(param.Name);
}
where the ParamCache{K} was a static dictionary with synchronized access that parsed the expression in order to get to the parameter name and argument retriever.
Well, after spending a little more time this morning in the Reflector and Visual Studio Immediate Window I have found out that there is more simple approach. And it allows the code to run 300 faster (100000000 operations in 1 second) by avoiding the overhead of reflection on the main execution path:
Enforce.Argument(() => args);
public static void Argument<K>(Func<K> argument) where K : class
{
if (null == argument())
throw ArgumentNull(argument);
}
Note that we do not even use an expression here, but rather a direct delegate.
Argument name retrieval is invoked only if the check fails. And it boils down to this function call (possibly with results cached in a dictionary and protected by the checks to ensure that we have proper IL):
static Exception ArgumentNull<K>(Func<K> argument)
{
// get IL code behind the delegate
var il = argument.Method.GetMethodBody().GetILAsByteArray();
// bytes 2-6 represent the field handle
var fieldHandle = BitConverter.ToInt32(il,2);
// resolve the handle
var field = argument.Target.GetType()
.Module.ResolveField(fieldHandle);
var name = field.Name;
var message = string.Format(
"Parameter of type '{0}' can't be null", typeof (K));
return new ArgumentNullException(name, message);
}
Here we get the raw delegate IL and extract field reference handle to the anonymous type (generated by the compiler) that is the target of the argument referenced by lambda. Then we simply get field that is referenced by the discovered metadata token. Name of the field matches with the one of the parameter.
Note, how we take our chance to make the message slightly more descriptive by reporting type of the argument as well.
All this results in exception being thrown like this:
Now, nothing stands in the way of simplifying the syntax of all Enforce sanity check helpers exposed by the Lokad Shared.
The primary article on How to Find Out Variable or Parameter Name in C# has, obviously, been updated with the overview of this approach.