Skip to main content

C# - Obtaining Method Caller Information [Intermediate]


You're hard at work developing a new logger for your project and you have a phone call. It's a call from home, it must be really late. "Oh, I only wish C# methods had a caller ID feature", you think to yourself. You could really use that information in your logger. Well, as of .NET 4.5 / Visual Studio 2012 this is possible! A method can now obtain information about the method that called it.

 

Usage

A method can obtain the following information about its caller:
  • Member name
  • Source file path
  • Line number
To get this information all we need to do is define optional parameters and decorate them with an attribute from the System.Runtime.CompilerServices namespace. The compiler will do the rest.
Let's see an example:
public static void Log(string msg, 
    [CallerMemberName] string memberName  = "", 
    [CallerFilePath] string filePath = "", 
    [CallerLineNumber] int lineNumber = 0)
{
    string msgToLog = string.Format("{0} ({1} line {2}): {3}",
        memberName, filePath, lineNumber, msg);
    Trace.WriteLine(msgToLog);
}
 
The simple Logger class above defines a single method - Log. The method accepts four parameters, the first of which is the message to be logged and the rest will automatically receive caller information.

Now let's use our logger:
class BusinessLogic
{
    public void PerformLogic()
    {
        // do logic
        Logger.Log("Finished performing logic.");
    }
}
 
As you can see, when we invoke the logger we only have to provide a value for the first parameter - the message. The rest of the parameters are automatically provided values by the compiler. The output when the above method executes will be similar to the following:
PerformLogic (c:\Project\Program.cs line 26): Finished performing logic.

To summarize, here is how we obtain caller information:
  • To obtain the member name, define an optional parameter of type string and decorate it with the CallerMemberName attribute
  • To obtain the source file path, define an optional parameter of type string and decorate it with the CallerFilePath attribute
  • To obtain the line number, define an optional parameter of type int and decorate it with the CallerLineNumber attribute
The order of the above parameters is insignificant and we aren't required to define all of them.

 

How Does it Work?

The short answer: with some help from our good ol' friend, the compiler. 

When the compiler encounters a call to a method that has parameters with the above attributes, and values to those parameter are omitted, it steps in and provides the values automatically.

Here is the IL code for the PerformLogic method:
.method public hidebysig instance void  PerformLogic() cil managed
{
  .maxstack  8
  IL_0000:  ldstr      "Finished performing logic."
  IL_0005:  ldstr      "PerformLogic"
  IL_000a:  ldstr      "c:\\Project\\Program.cs"
  IL_000f:  ldc.i4.s   26
  IL_0011:  call       void ConsoleApplication1.Logger::Log(string,
                                                            string,
                                                            string,
                                                            int32)
  IL_0016:  ret
} // end of method BusinessLogic::PerformLogic

As you can easily see, the member name, source file path and line number are specified as literal values. In fact, once the compiler is done, there is no evidence that something special ever happened here. This looks just like another method call with literal values as parameters. Nice...

 

Implementing the INotifyPropertyChanged Interface

Another scenario where obtaining method caller information is very useful is when implementing the INotifyPropertyChanged interface, which is used for data binding. It allows a property to notify that its value has changed. The interface is defined as follows:
public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}
 
The interface defines an event, which needs to be fired when a property changes. Consider the following implementation of INotifyPropertyChanged:
class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string firstName;
    private string lastName;

    public string FirstName
    {
        get { return this.firstName; }
        set
        {
            this.firstName = value;
            NotifyPropertyChanged("FirstName");
        }
    }

    public string LastName
    {
        get { return this.lastName; }
        set
        {
            this.lastName = value;
            NotifyPropertyChanged("LastName");
        }
    }

    private void NotifyPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
 
The person class defines two properties: FirstName and LastName. To support data binding, it implements the INotifyPropertyChanged interface, which requires that it defines the PropertyChanged event. Whenever the value of a property changes, the event is fired and is provided with the name of that property. The setter of the properties above, calls the private NotifyPropertyChanged method, which fires the event.

Now imagine that you decide to rename the LastName property to Surname. Note that when the setter of LastName calls the NotifyPropertyChanged it provides it with the name of the property as a string literal. When renaming the property to Surname, we must also remember to modify the string literal accordingly. Failing to do so may cause the binding to fail. I'm sure you can appreciate how error prone this approach can be... But until recently, it was our only option. Not any more! Consider the following revised code (with only the relevant parts for simplification):
public string LastName
{
    get { return this.lastName; }
    set
    {
        this.lastName = value;
        NotifyPropertyChanged();
    }
}

private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
    if (this.PropertyChanged != null)
    {
        this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}
 
Now the NotifyPropertyChanged uses the CallerMemberName attribute to have the compiler provide the member name (in this case the property name) automatically. See how the call to CallerMemberName inside the setter of the property no longer needs to specify the property name? Now if we rename the LastName property to Surname, data binding is guaranteed to continue working properly. Is that cool or what?

 

An Alternative to Caller Information (and why it isn't as good)

This feature is new to .NET 4.5 / Visual Studio 2012. If you're using a previous version, you need to find an alternative. You can use the System.Diagnostics.StackFrame class to obtain information about the calling method at runtime. Consider this revised Log method:
public static void Log(string msg)
{
    StackFrame stackFrame = new StackFrame(1);
    string methodName = stackFrame.GetMethod().Name;
    string fileName = stackFrame.GetFileName();
    int lineNumber = stackFrame.GetFileLineNumber();

    string msgToLog = string.Format("{0} ({1} line {2}): {3}",
        methodName, fileName, lineNumber, msg);
    System.Diagnostics.Trace.WriteLine(msgToLog);
}
 
Here we instantiate a StackFrame and provide it with a value of 1, to get information about the method one frame up the call stack. We then use it to extract the method name, file name and line number.

This approach uses information that is available at runtime and therefore suffers from the following drawbacks:
  • The file name and the line number are typically retrieved from the debugging symbols, which aren't always available
  • If using an obfuscator, methods may be renamed to meaningless, cryptic names; these obfuscated names will be available through StackFrame rather than the original name
  • When the JIT compiler compiles IL to native code, it may inline simple methods to improve performance, in which case the method one frame up the call stack may be different than the method we expect it to be based on the source code; in fact, using the StackFrame approach to implement INotifyPropertyChanged is almost guaranteed to fail, because properties are such great candidates for inlining, due to their typical simplicity
  • Instantiating and using a StackFrame is extra work that has to be done at runtime and takes processing cycles

Using method caller information has none of the above drawbacks. The information is emitted by the compiler and appears in the IL code as literals. This means that this information is guaranteed to exist, is resilient to obfuscation and method inlining, and is as efficient as accessing a parameter.

 

Final Words

There's one thing that I feel is missing from the implementation of this cool new feature. I wonder why there isn't another attribute to allow access to the fully qualified name of the type that contains the calling method. This could be useful in logging / diagnostics scenarios. But overall this is an excellent new feature, which I hope you will find as useful as I have.

Comments

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >();

C# Snippet - The Many Uses Of The Using Keyword [Beginner]

What is the first thing that pops into your mind when you think of the using keyword for C#? Probably those lines that always appear at the top of C# code files - the lines that import types from other namespaces into your code. But while that is the most common use of the using keyword, it is not the only one. Today we are going to take a look at the different uses of the using keyword and what they are useful for. The Using Directive There are two main categories of use for the using keyword - as a "Using Directive" and as a "Using Statement". The lines at the top of a C# file are directives, but that is not the only place they can go. They can also go inside of a namespace block, but they have to be before any other elements declared in the namespace (i.e., you can't add a using statement after a class declaration). Namespace Importing This is by far the most common use of the keyword - it is rare that you see a C# file that does not h

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as