C# is a statically typed language, and one of the greatest derived strengths is our ability to ask questions around our code. Whether you love it or hate it, reflection is a critical part of the .NET stack, and when you program long enough, you’re bound to run into it. What if I told you there are other ways to get information about your executing application? In this post, we’ll take a look at the Caller* family of attributes, which will allow you to get information like file path, line number, member name, and argument expression. You can find all of these methods in the System.Runtime.CompilerServices namespace.

Using CallerFilePathAttribute

You can apply the CallerFilePathAttribute to an optional string argument in any method. It’s best practice to add the argument to the end of your method call so the argument doesn’t get in the way of the intent of the method. Let’s take a look at an example.

using System.Runtime.CompilerServices;

FilePath();

static void FilePath([CallerFilePath] string filepath = "")
{
    Console.WriteLine(filepath);
}

Running our application, we can see the file path in the console.

/Users/khalidabuhakmeh/Projects/Dotnet/ConsoleApp9/ConsoleApp9/Program.cs

Using CallerMemberNameAttribute

Understanding what member invoked the current method can help trace the source of an issue. Using the CallerMemberNameAttribute, we can determine the member name of the caller. Let’s look at a sample.

using System.Runtime.CompilerServices;

WhoThere();

static void WhoThere([CallerMemberName] string name = "")
{
    Console.WriteLine(name);
}

Being called from a top-level statements program, we can see the name of the generated primary method.

<Main>$

Using CallerLineNumberAttribute

As you may have guessed from the name, you can retrieve the line number from the calling member. Like previous attributes, you can apply the attribute to an optional argument, but this time, the argument type should be an int.

using System.Runtime.CompilerServices;

WhereYouAt();

static void WhereYouAt([CallerLineNumber] int lineNumber = 0)
{
    Console.WriteLine(lineNumber);
}

The result of our code execution is 3. Note that the result takes into account all empty lines.

Using CallerArgumentExpressionAttribute

Probably the most exciting of all the attributes, CallerArgumentExpressionAttribute can get the expression passed into a method. The results can include the parameter’s name or the expression that could result in an argument value. It’s probably easier to see the attribute in action. You need at least one other argument and its name passed to the attribute’s constructor to use the attribute.

using System.Runtime.CompilerServices;

var math = 2 * 2;
ExpressYourself(math);

ExpressYourself(2 + 2);

static void ExpressYourself(
    int result, 
    [CallerArgumentExpression("result")] string expression = "")
{
    Console.WriteLine($"{expression} is {result}");
}

Unlike the other attributes, note the use of the argument "result" being used with CallerArgumentExpression. Running our code, we see the following results.

math is 4
2 + 2 is 4

Conclusion

We can use these attributes to get metadata about a running application, but one drawback is that it can pollute our method definitions. The most compelling attribute is CallerArgumentExpression, which we could use to understand parameter names without the need to pass an Expression and use complex reflection code. I hope you learned something, and let me know how you use these attributes in your codebase.

Resources