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

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)
                "[{0:g} - {1} - {2} - line {3}] {4}",

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

22 thoughts on “Using C# 5 caller info attributes when targeting earlier versions of the .NET framework”

  1. 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?

    1. 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.

      1. 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.

        1. 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.

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

          2. @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

          3. 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. 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?

      1. 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?

  3. 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()
    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

    1. I am not able to add Global to the start of the namespace. The compiler complains: “‘Global’ not allowed in this context; identifier expected”

      When I remove Global from the namespace then I need to add my project default namespace to ‘System.Runtime.CompilerServices’ to have access to the classes e.g. CallerMemberNameAttribute etc…

      Any ideas?

      1. @Scott, which version of VS are you using? If I understand correctly, this feature was added in VS2012 (VB11)

    2. Christian – I had to add before Optional memberName, etc before your example would work. Thanks for the tip, though.

  4. 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.

  5. Thank you, this is very helpful. However I find it interesting to know then how this works. Like how does the compiler know how this attribute should work if it isn’t in .net4.0, i understand it is part of c#5, but how does the compiler understands the link, is it just by name matching of the attribute?

    1. Hi Ahmad,

      The compiler looks for the attributes only by name (namespace + type name), not by assembly. So it doesn’t matter in which assembly attributes are located, as long as the compiler can find them in the expected namespace.

Leave a Reply

Your email address will not be published. Required fields are marked *