Extract calling method metadata with C#

Extract useful information from a calling method

Home DailyDrop

Daily Knowledge Drop

As part of the C# System.Runtime assembly, there are a number of Attributes which can be used to get information about the caller of a method.

The pattern for usage is the same for all attributes. To extract the metadata when a method is called, the following needs to be done:

  • A new parameter needs to be added to the method (the type of the parameter depends on the attribute, see examples below)
  • The new parameter must have a default value
  • The new parameter must be decorated with the relevant Attribute

Attributes

Lets look at a few examples. All examples built using .NET6 minimal console application.

Line Number

To get the caller line number, the CallerLineNumber attribute is used.

LogMessageWithLineNumber("Message 1");

static void LogMessageWithLineNumber(string message, [CallerLineNumber] int lineNumber = 0)
{
    Console.WriteLine($"LineNumber={lineNumber}: {message}");
}

In the method signiture above, we can see a decorated parameter, with a default value has been added. This field will automatically be populated with the line number of the calling method

The output is as follows:

LineNumber=4: Message 1

File Path

To get the file path of the calling method, the CallerFilePath attribute is used.

LogMessageWithPath("Message 2");

static void LogMessageWithPath(string message, [CallerFilePath] string filepath = "")
{
    Console.WriteLine($"Path={filepath}: {message}");
}

The output is as follows:

Path=C:\Development\Projects\Blog\CallerMetadata\CallerMetadata\Program.cs: Message 2

Method Name

To get the calling method name, the CallerMemberName attribute is used.

LogMessageWithPath("Message 2");

static void LogMessageWithMethod(string message, [CallerMemberName] string memberName = "")
{
    Console.WriteLine($"Method={memberName}: {message}");
}

The output is as follows:

Method=<Main>$: Message 3

As the example is using the .NET6 Console minimal project, there is no explicitly defined main method - hence the method reflecting as <Main>$

Lets look at another example.

new ClassCall().DoubleHop();

class ClassCall
{
    public void DoubleHop()
    {
        LogMessageWithMethod("Message from class");
    }

    static void LogMessageWithMethod(string message, 
        [CallerMemberName] string memberName = "")
    {
        Console.WriteLine($"Method={memberName}: {message}");
    }
}

In this example, the source of the call to LogMessageWithMethod is the DoubleHop method, as such the output is as follows:

Method=DoubleHop: Message from class

Expression

The last attribute is the most interesting and the most useful. The CallerArgumentExpression attribute is used to get the expression used as the argument to the method.

In the following example, the same string message "Message 4" will be logged, however the string will be generated differently each time the method is invoked.

// Just send a string
LogMessageWithExpression("Message 4");
// Add two string together with the + operator
LogMessageWithExpression("Message" + " 4");
// Concatenate two string using Concat method
LogMessageWithExpression(String.Concat("Message", " 4"));

static void LogMessageWithExpression(string value, 
    [CallerArgumentExpression("value")] string expression = "")
{
    Console.WriteLine($"{{{value}}} was generated by {{{expression}}}");
}

In this case, the CallerArgumentExpression attribute takes a parameter itself, and that is the name of the parameter for which is should get the caller argument expression.

The output is as follows:

{Message 4} was generated by {"Message 4"}
{Message 4} was generated by {"Message" + " 4"}
{Message 4} was generated by {String.Concat("Message", " 4")}

We can see from the output, that the expression parameter gets populated with the code used to generate the parameter value (the expression for the parameter argument).

This can be invaluable when logging, as not only can a piece of information be logged, but also which piece of code is generating the information.

Overriding parameter values

In all of the above examples, as mentioned, the parameter decorated with the relevant attribute needs to have a default value. So what happens if a value is explicitly supplied for the parameter?
As one might expect, instead of using the default and allowing the attribute to populate the parameter, the explicitly supplied value is used.

Here a value is explicitly supplied for the memberName parameter:

LogMessageWithMethod("Message 5", "OwnMethodName");

static void LogMessageWithMethod(string message, 
    [CallerMemberName] string memberName = "")
{
    Console.WriteLine($"Method={memberName}: {message}");
}

The output, as expected:

Method=OwnMethodName: Message 5

Notes

While simple to implement, using these features can be very intrusive, with the need to add additional parameters to relevant methods. However if required, these tools can provide invaluable information about the calling location.


References

Get C# Metadata From a Call Site

Daily Drop 09: 11-02-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net metadata call stack attribute