Knowing When To Reflect With Caller Info Attributes

If you've been around C# long enough, you've probably had a need at one time or another to figure out some information about code that's being executed. It might be a method name, the file being executed, or even more specific details like the line number.
 
These things can be accessed through Reflection, as you might expect, but that might not be the best way to go. This post will discuss some of the available language features in C# to help you avoid resorting to it if you don't have to.
 

Why Not Reflection?

 
Reflection is quite a bit like using Entity Framework; both technologies are quite powerful, but there are tons of potentially nasty things going on behind the scenes that you probably don't want happening in the first place.
 
Performance can be a major concern when Reflection is involved as it requires huge amounts of metadata about the current code to be loaded and processed. According to this post by fellow Microsoft MVP Rick Strahl, it could result in roughly 4x slower processing times (in some cases).
 
Now performance numbers should always be taken with a grain of salt. If you are talking processing times that are running in a matter of milliseconds, then a 4x increase might be trivial; likewise, if you are talking minutes, then it's a different story.
 
The gist is this: if Reflection can provide you the solution you need, without compromising performance, then use it - assuming some of the later sections of the post don't do it better.
 

Calling All Attributes

 
Let's say you wanted to get the name of a method (it could be a class, property, etc.) for logging purposes. For demonstration purposes, let's just say it looks like this,
  1. public void LogMethodName(string methodName)    
  2. {  
  3.      // This isn't important  
  4. }  
Now in the past, if we wanted to retrieve the name of the caller method, we might have to resort to something like this,
  1. // Using System.Diagnostics  
  2. public void SomethingHappened()    
  3. {  
  4.     // Get the current method name (to log)   
  5.     var method = new StackTrace().GetFrame(0).GetMethod().Name;  
  6.   
  7.     // Now log it  
  8.     LogMethodName(method);  
  9. }  
or,
  1. // Using System.Reflection  
  2. public string SomethingHappened()    
  3. {  
  4.     var method = MethodBase.GetCurrentMethod().Name;  
  5.     LogMethodName(method);  
  6. }  
I know, they are gross. Let's turn to some of the Caller Info attributes that were introduced within .NET 4.5, namely [CallerMemberAttribute]. Unlike the other options, this will simply make a minor change to our Log() method, which wouldn't require us to pass in a method name (or go through the work of resolving it at all),
  1. // Using System.Runtime.CompilerServices  
  2. public void LogMethodName([CallerMemberName] string methodName = null)    
  3. {  
  4.      // methodName will always have our caller  
  5. }  
This attribute leverages the fact that the compiler actually knows what everything is at compile-time; as such it will emit specific values as literals into the Intermediate Language (IL).
 
The benefits,
  • It's easy to implement- If you have your code designed in such a way that you can take advantage of the attribute, it doesn't require massive blocks of code in all your methods prior to calling the one you care about.
  • It's flexible and easily overridden- Since the CallerMemberName (and other caller info attributes) require an optional parameter, you can always pass one in to hide the caller information.
  • It's WAY faster- Since the caller information is available at compile-time: it's already there. You don't have to process any metadata or dive through the frames of a StackTrace.
     
    Attributes
A quick example comparing these three approaches yielded the following results (over 1 million iterations),
  • Reflection- 1668ms
  • StackTrace- 7667ms
  • CallerMemberName- 1ms
As you can see, CallerMemberName is orders of magnitude faster than either of the other two options, in addition to being far more efficient memory-wise.
 
You might ask, how in the world is this so much faster? Well, let's look behind the scenes at what the generated IL looks like and we'll see why.
 
Consider the following program.
  1. class Program    
  2. {  
  3.     static void Main(string[] args)  
  4.     {  
  5.         SomeMethod();  
  6.     }  
  7.   
  8.     private static void SomeMethod()  
  9.     {  
  10.         LogSomething();  
  11.     }  
  12.   
  13.     private static void LogSomething([CallerMemberName] string method = null)  
  14.     {  
  15.         // Write to log  
  16.     }  
  17. }  
If we look at the IL generated by this, we'll get our performance answer,
 
Attributes
 
As you can easily see, it's fast because we don't have to look up anything. The string itself is present directly in the IL itself as opposed to making a long chain of Reflection calls or unraveling a cumbersome Stack Trace.
 

More Than a  Method

 
These compile-time attributes aren't limited to only method and property names. You can also access other caller information such as file names and line numbers (both of which are resolved at compile-time) through the following attributes:
  • [CallerMemberName]- Sets the parameter to the name of the method or property making the call.
  • [CallerFilePath]- Sets the parameter to the full file path (at compile-time) of the call prior.
  • [CallerLineNumber]- Sets the parameter to the line number of the source file (at compile time) making the call.
As you can imagine, these attributes could easily be used to create a robust, performant logging system that didn't require anything to be passed to the log method itself,
  1. public void LogToDatabase([CallerFilePath] string callingFilePath,[CallerMemberName] string callingMember = null, [CallingLineNumber] int? = null)    
  2. {  
  3.     // Do work  
  4. }  
While these attributes might not always be useful, they could be an excellent option if you are working with a legacy codebase that might be using Reflection or StackTraces to resolve some of this information.
 
I'd be interested to see if anyone was venturing outside the box and using these attributes to do any wild stuff. If you think you are - feel free to leave a comment.