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 theCallerMemberName
attribute - To obtain the source file path, define an optional parameter of type
string
and decorate it with theCallerFilePath
attribute - To obtain the line number, define an optional parameter of type
int
and decorate it with theCallerLineNumber
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 implementINotifyPropertyChanged
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
Post a Comment