Underrated .NET Framework Attributes

Thursday, 10 August 2023
.NET

If you are working on the .NET framework, you might have come across classes and types which you haven't seen or heard before. They are usually hidden from your eyes just because they are not used in your day-to-day development work. Although you might not use them very often, they could be so handy when the right time comes.

One namespace which contains lots of interesting classes is "System.Runtime.CompilerServices". In this post, we are going to review the following attributes:

CallerMemberNameAttribute
CallerFilePathAttribute
CallerLineNumberAttribute
CallerArgumentExpressionAttribute

All of them attach to method parameters and fill their associated parameters with a compile-time value. Since values for the parameters are generated by the compiler, they MUST be optional so that they can be skipped when writing the method call.

CallerMemberNameAttribute, CallerFilePathAttribute, and CallerLineNumberAttribute are pretty straightforward. They provide the caller method's name, the caller method's source file path, and the line number in which the caller method calls this method respectively.

CallerArgumentExpressionAttribute on the other hand, is a little tricky. Unlike the other three, this attribute accepts a parameter name (let's call it the source parameter) and It fills its accompanying parameter with the expression which is passed to the source parameter at the call site.

To understand these attributes, there is no way better than seeing them in action. Consider the following class:

public static class Calculator
{
    public enum Operations
    {
        Addition,
        Subtraction,
        Multipication,
        Division
    }

    public static long Calculate(
        int value1, int value2, Operations operation,
        [CallerMemberName] string callerName = "",
        [CallerFilePath] string callerFile = "",
        [CallerLineNumber] int callerLine = 0,
        [CallerArgumentExpression(nameof(value1))] string value1Expression = "",
        [CallerArgumentExpression(nameof(value2))] string value2Expression = "",
        [CallerArgumentExpression(nameof(operation))] string operationExpression = ""
    )
    {
#if DEBUG
        Console.WriteLine("------------ BEGIN DEBUG DATA ------------");
        Console.WriteLine("Caller's Name = {0}", callerName);
        Console.WriteLine("Caller's File = {0}", callerFile);
        Console.WriteLine("Caller's Line = {0}", callerLine);
        Console.WriteLine("First value expression = {0}", value1Expression);
        Console.WriteLine("Second value expression = {0}", value2Expression);
        Console.WriteLine("Operation expression = {0}", operationExpression);
        Console.WriteLine("------------ END DEBUG DATA ------------");
        Console.WriteLine();
#endif

        return operation switch
        {
            Operations.Addition => value1 + value2,
            Operations.Subtraction => value1 - value2,
            Operations.Multipication => value1 * value2,
            Operations.Division => value1 / value2,
            _ => throw new InvalidOperationException()
        };
    }
}

To utilize the Calculator class, we can use the next class:

public static class CalculatorTest
{
    public static void PerformTests()
    {
        int a = 12, b = 4;
        Calculator.Calculate(20, 25, Calculator.Operations.Addition);
        Calculator.Calculate(20 + 12, 25 - 4, Calculator.Operations.Subtraction);
        Calculator.Calculate(Int32.Parse("3"), 2, Calculator.Operations.Multipication);
        Calculator.Calculate(a, b, Calculator.Operations.Division);
    }
}

which produces the following output:

------------ BEGIN DEBUG DATA ------------
Caller's Name = PerformTests
Caller's File = D:\Calculator\CalculatorTest.cs
Caller's Line = 8
First value expression = 20
Second value expression = 25
Operation expression = Calculator.Operations.Addition
------------ END DEBUG DATA ------------

------------ BEGIN DEBUG DATA ------------
Caller's Name = PerformTests
Caller's File = D:\Calculator\CalculatorTest.cs
Caller's Line = 9
First value expression = 20 + 12
Second value expression = 25 - 4
Operation expression = Calculator.Operations.Subtraction
------------ END DEBUG DATA ------------

------------ BEGIN DEBUG DATA ------------
Caller's Name = PerformTests
Caller's File = D:\Calculator\CalculatorTest.cs
Caller's Line = 10
First value expression = Int32.Parse("3")
Second value expression = 2
Operation expression = Calculator.Operations.Multipication
------------ END DEBUG DATA ------------

------------ BEGIN DEBUG DATA ------------
Caller's Name = PerformTests
Caller's File = D:\Calculator\CalculatorTest.cs
Caller's Line = 11
First value expression = a
Second value expression = b
Operation expression = Calculator.Operations.Division
------------ END DEBUG DATA ------------

Notice the use of #if DEBUG. It allows us to enable/disable the debug data generation based on the current compilation mode.

Top