Using C# 5 caller info attributes when targeting earlier versions of the .NET framework

Very poorPoorAverageGoodExcellent (4 votes) 
Loading ... Loading ...

Caller info attributes are one of the new features of C# 5. They’re attributes applied to optional method parameters that enable you to pass caller information implicitly to a method. I’m not sure that description is very clear, so an example will help you understand:

        static void Log(
            string message,
            [CallerMemberName] string memberName = null,
            [CallerFilePath] string filePath = null,
            [CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine(
                "[{0:g} - {1} - {2} - line {3}] {4}",
                DateTime.UtcNow,
                memberName,
                filePath,
                lineNumber,
                message);
        }

The method above takes several parameters intended to pass information about the caller: calling member name, source file path and line number. The Caller* attributes make the compiler pass the appropriate values automatically, so you don’t have to specify the values for these parameters:

        static void Foo()
        {
            Log("Hello world");
            // Equivalent to:
            // Log("Hello world", "Foo", @"C:\x\y\z\Program.cs", 18);
        }

This is of course especially useful for logging methods…

Notice that the Caller* attributes are defined in the .NET Framework 4.5. Now, suppose we use Visual Studio 2012 to target an earlier framework version (e.g. 4.0): the caller info attributes don’t exist in 4.0, so we can’t use them… But wait! What if we could trick the compiler into thinking the attributes exist? Let’s define our own attributes, taking care to put them in the namespace where the compiler expects them:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerMemberNameAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerFilePathAttribute : Attribute
    {
    }

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public class CallerLineNumberAttribute : Attribute
    {
    }
}

If we compile and run the program, we can see that our custom attributes are taken into account by the compiler. So they don’t have to be defined in mscorlib.dll like the “real” ones, they just have to be in the right namespace, and the compiler accepts them. This enables us to use this cool feature when targeting .NET 4.0, 3.5 or even 2.0!

Note that a similar trick enabled the creation of extension methods when targeting .NET 2.0 with the C# 3 compiler: you just had to create an ExtensionAttribute class in the System.Runtime.CompilerServices namespace, and the compiler would pick it up. This is also what enabled LinqBridge to work.

kick it on DotNetKicks.com

14 Comments

  1. Akku says:

    Tried this in .NET 4.0 Visual Studio 2010 on Windows 7 and it didn”t work. Do I need to think about anything more than copy pasting your namespace code into one of my files and using the attributes? Do I need a special version of the compiler?

    • Thomas Levesque says:

      Hi Akku, you need C# 5 for this to work; VS 2010 uses the C# 4 compiler, so it can”t work. This trick is only useful for VS2012 projects that target .NET 4.

      • Akku says:

        Ah, okay, thanks a lot for this clarification.

        For someone reading this and wondering, you can of course use regular reflection e.g. using new System.Reflection.StackFrame(1).GetMethod() in C# 4.0.

        • Thomas Levesque says:

          Yes, but be careful with the stack technique: if the method is inlined, it won”t show up in the stack, so it StackFrame.GetMethod will return the caller.

          • Richard says:

            But using System.Runtime.CompilerServices.MethodImplAttribute with MethodImplOptions.NoInlining can prevent the method being inlined thus mitigating that problem

          • Thomas Levesque says:

            @Richard, yes, but there are other issues with this technique:

            - the calling method itself (not the log method) could be inlined, and putting MethodImplAttribute on every method probably isn’t such a good idea
            - if the assembly is compiled without debug information, you won’t get the file name and line number

          • vcsjones says:

            Mind you also that the x64 JIT may convert your call into a .tail call, which isn’t the same as being inlined, but modifies your stacktrace and gives the appearance of being inlined. You can mitigate this by using MethodImplOptions.NoOptimization, but now you may be taking a big performance hit, and it doesn’t completely solve the problem with the caller’s caller.

  2. Frank says:

    Great work!

  3. Manish says:

    I tried it in VS2012 targeting framework 4.0 but all I get is empty file path,line as well as member Name.
    Do I missing something?

    • Manish says:

      Do you tried it Frank coz its not working here mate.

      • Thomas Levesque says:

        Yes, I tried it, and it works fine. I don”t usually post something on my blog without testing it first ;)

        Are you sure you declared the attributes in the correct namespace?

  4. Christian says:

    This also works in VB.NET. However, you need to prefix the namespaces with “Global”, because VB.NET has a different way how it handles namespace.


    Example (console application for VS 2012, VB.NET based, targetting .NET 4.0):

    Imports System.Runtime.CompilerServices
    Imports System.Diagnostics

    Module Module1

    Sub Main()
    DoProcessing()
    End Sub

    Private Sub DoProcessing()
    TraceMessage(“Something happened.”)
    End Sub

    Public Sub TraceMessage(message As String,
    Optional memberName As String = Nothing,
    Optional sourcefilePath As String = Nothing,
    Optional sourceLineNumber As Integer = 0)

    Trace.WriteLine(“message: ” & message)
    Trace.WriteLine(“member name: ” & memberName)
    Trace.WriteLine(“source file path: ” & sourcefilePath)
    Trace.WriteLine(“source line number: ” & sourceLineNumber)
    End Sub
    End Module

    Namespace Global.System.Runtime.CompilerServices

    Public NotInheritable Class CallerMemberNameAttribute
    Inherits Attribute
    End Class

    Public NotInheritable Class CallerFilePathAttribute
    Inherits Attribute
    End Class

    Public NotInheritable Class CallerLineNumberAttribute
    Inherits Attribute
    End Class
    End Namespace

  5. Thomas, I’m a very straight man but I could just kiss you for figuring this out with these attributes. v4.5 seems such an eternally distant ambition because people just won’t let XP die.

    I just discovered this blog but holy crap it’s good to find someone who loves .NET like I do, especially now that Josh Smith went to the dark side.

1 Trackbacks

Leave a comment

css.php