Once again, in this series of posts I look at the parts of the .NET Framework that may seem trivial, but can help improve your code by making it easier to write and maintain. The index of all my past little wonders posts can be found here.
There are times when it is desirable to know who called the method or property you are currently executing. Some applications of this could include logging libraries, or possibly even something more advanced that may server up different objects depending on who called the method.
In the past, we mostly relied on the System.Diagnostics namespace and its classes such as StackTrace and StackFrame to see who our caller was, but now in C# 5, we can also get much of this data at compile-time.
Determining the caller using the stack
One of the ways of doing this is to examine the call stack. The classes that allow you to examine the call stack have been around for a long time and can give you a very deep view of the calling chain all the way back to the beginning for the thread that has called you.
You can get caller information by either instantiating the StackTrace class (which will give you the complete stack trace, much like you see when an exception is generated), or by using StackFrame which gets a single frame of the stack trace. Both involve examining the call stack, which is a non-trivial task, so care should be done not to do this in a performance-intensive situation.
For our simple example let’s say we are going to recreate the wheel and construct our own logging framework. Perhaps we wish to create a simple method Log which will log the string-ified form of an object and some information about the caller.
We could easily do this as follows:
1: static void Log(object message)
2: {
3: // frame 1, true for source info
4: StackFrame frame = new StackFrame(1, true);
5: var method = frame.GetMethod();
6: var fileName = frame.GetFileName();
7: var lineNumber = frame.GetFileLineNumber();
8:
9: // we'll just use a simple Console write for now
10: Console.WriteLine("{0}({1}):{2} - {3}",
11: fileName, lineNumber, method.Name, message);
12: }
So, what we are doing here is grabbing the 2nd stack frame (the 1st is our current method) using a 2nd argument of true to specify we want source information (if available) and then taking the information from the frame.
This works fine, and if we tested it out by calling from a file such as this:
1: // File c:\projects\test\CallerInfo\CallerInfo.cs
2:
3: public class CallerInfo
4: {
5: Log("Hello Logger!");
6: }
We’d see this:
1: c:\projects\test\CallerInfo\CallerInfo.cs(5):Main - Hello Logger!
This works well, and in fact CallStack and StackFrame are still the best ways to examine deeper into the call stack. But if you only want to get information on the caller of your method, there is another option…
Determining the caller at compile-time
In C# 5 (.NET 4.5) they added some attributes that can be supplied to optional parameters on a method to receive caller information. These attributes can only be applied to methods with optional parameters with explicit defaults. Then, as the compiler determines who is calling your method with these attributes, it will fill in the values at compile-time.
These are the currently supported attributes available in the System.Runtime.CompilerServices namespace”:
- CallerFilePathAttribute – The path and name of the file that is calling your method.
- CallerLineNumberAttribute – The line number in the file where your method is being called.
- CallerMemberName – The member that is calling your method.
So let’s take a look at how our Log method would look using these attributes instead:
1: static int Log(object message,
2: [CallerMemberName] string memberName = "",
3: [CallerFilePath] string fileName = "",
4: [CallerLineNumber] int lineNumber = 0)
5: {
6: // we'll just use a simple Console write for now
7: Console.WriteLine("{0}({1}):{2} - {3}",
8: fileName, lineNumber, memberName, message);
9: }
Again, calling this from our sample Main would give us the same result:
1: c:\projects\test\CallerInfo\CallerInfo.cs(5):Main - Hello Logger!
However, though this seems the same, there are a few key differences.
First of all, there are only 3 supported attributes (at this time) that give you the file path, line number, and calling member. Thus, it does not give you as rich of detail as a StackFrame (which can give you the calling type as well and deeper frames, for example).
Also, these are supported through optional parameters, which means we could call our new Log method like this:
1: // They're defaults, why not fill 'em in
2: Log("My message.", "Some member", "Some file", -13);
In addition, since these attributes require optional parameters, they cannot be used in properties, only in methods.
These caveats aside, they do let you get similar information inside of methods at a much greater speed!
How much greater? Well lets crank through 1,000,000 iterations of each. instead of logging to console, I’ll return the formatted string length of each. Doing this, we get:
1: Time for 1,000,000 iterations with StackTrace: 5096 ms
2: Time for 1,000,000 iterations with Attributes: 196 ms
So you see, using the attributes is much, much faster! Nearly 25x faster in fact.
Summary
There are a few ways to get caller information for a method. The StackFrame allows you to get a comprehensive set of information spanning the whole call stack, but at a heavier cost. On the other hand, the attributes allow you to quickly get at caller information baked in at compile-time, but to do so you need to create optional parameters in your methods to support it.
Technorati Tags: LittleWonders,CSharp,C#,.NET,StackFrame,CallStack,CallerFilePathAttribute,CallerLineNumberAttribute,CallerMemberName