Popular Categories
« ReSharper 4.5 Features and Release Date | Main | Parameter vs. Argument »
Friday
Dec192008

How to Get Parameter Name and Argument Value From C# Lambda via IL?

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:

Exception being thrown by the sanity check

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.

Reader Comments (10)

I like the idea but I don't understand why "ArgumentNotEmpty" raises an ArgumentNullException if you pass in empty string?

January 13, 2009 | Unregistered CommenterColin Jack

Thanks for pointing this out!

Fixed this (null throws ArgumentNull, while Length==0 throws Argument) in the latest release (1.1.1715.1)

January 13, 2009 | Registered CommenterRinat Abdullin

I'm also not finding the method names very descriptive, for example "Enforce.Argument" doesn't tell me much about what we're enforcing.

January 13, 2009 | Unregistered CommenterColin Jack

Thanks for the feedback!

I've tried to provide some explanation in a separate post.

Does it make sense to you?

January 14, 2009 | Registered CommenterRinat Abdullin

Do you have some smart way of getting the name (and possibly namespace) of the type where that argument comes from? Not sure if it is possible... but you seem like a smart guy, and that code up there works great :) Very cool.

For example, if I have a class named SomeClass, in the namespace Some.Namespace with for example a string named SomeString. If I then have a method like this somewhere: SomeMethod<T>(Func<T> argument) { ... }, and called it with (() => SomeString) as argument... I could then find out that that SomeString is, in fact called SomeString, like you showed here. But can I also find out that it is from an object of the SomeClass class which is in the Some.Namespace namespace?

January 15, 2009 | Unregistered CommenterSvish

Svish,

unfortunately this is not possible. When compiler grabs the arguments for the lambda, it merely copies their values to the fields in a new anonymous class. These fields happen to have the name of the original variable (that's why we can get the name out). But it is not possible to tell where the actual value came from (whether it was a property of class or something else).

Well, unless you do some really low-level IL parsing with Mono.Cecil (but that's going to have bad performance)

January 16, 2009 | Registered CommenterRinat Abdullin

Ah. Ok, thank you for the info.

Something I just ran into with your code... When sending in () => someobject.SomeProperty, your method returns someobject as the name. why is that? can there be done something to change so that it gets the other one? Or how would I go ahead if I wanted to get a string with, in this case, SomeProperty?

January 16, 2009 | Unregistered CommenterSvish

Check out System.Reflection.Reflect in the Lokad Shared Libraries (latest version). It has methods (properly implemented) to reflect properties and variables via IL.

January 16, 2009 | Registered CommenterRinat Abdullin

Hi there,

I ran into a problem using code like this. If the type or method has generic type arguments, the call to ResolveField throws a Bad Image Format exception.

Instead of:

var field = argument.Target.GetType().Module.ResolveField(fieldHandle);

You may be better doing something like:

Type expressionType = expression.Target.GetType ();
Type [] genericTypeArguments = expressionType.GetGenericArguments ();
Type [] genericMethodArguments = expression.Method.GetGenericArguments ();
var field = expressionType.Module.ResolveField (fieldHandle, genericTypeArguments, genericMethodArguments);

Hope this helps,

Geoff

January 24, 2009 | Unregistered CommenterGeoff Taylor

Geoff,

thank you for the feedback! I didn't even think of that scenario. Will add it to the unit tests.

January 26, 2009 | Registered CommenterRinat Abdullin
Comments for this entry have been disabled. Additional comments may not be added to this entry at this time.