CallerArgumentExpression Attribute in C# Logging

This article explores the CallerArgumentExpression attribute introduced in C# 10, explaining how it works and how it simplifies diagnostic logging. You will learn how this attribute captures the source-code representation of passed arguments at compile time, eliminating boilerplate code and significantly improving log clarity during application debugging.

What is the CallerArgumentExpression Attribute?

Introduced in .NET 6 and C# 10, CallerArgumentExpressionAttribute resides in the System.Runtime.CompilerServices namespace. It instructs the C# compiler to capture the exact code expression passed to another parameter as a string literal.

Like other caller information attributes ([CallerMemberName], [CallerFilePath], and [CallerLineNumber]), CallerArgumentExpression operates at compile time. It assigns the text representation of the source code passed to an argument to an optional parameter on the same method.

How It Assists with Logging

Traditional logging often requires developers to manually specify variable names or expressions using nameof() or string literals. This practice introduces boilerplate code, increases maintenance overhead, and creates risks of typos or mismatched logging messages if expressions change.

CallerArgumentExpression solves this problem by automating the capture of variable names, logical conditions, and expressions directly from the call site. It assists logging in three main areas:

  1. Assertion and Guard Clause Logging: It allows validation helpers to log the exact condition or variable that caused a failure without requiring manual string parameters.
  2. Context-Rich Debug Logging: It logs both the evaluation result and the expression itself, giving developers immediate context on why a branch was executed.
  3. Audit and Performance Logging: It records what operation or query was executed without having to pass a separate description parameter.

Practical Implementation Example

Here is a practical demonstration of how to implement a custom logging utility using CallerArgumentExpression:

using System;
using System.Runtime.CompilerServices;

public static class DiagnosticLogger
{
    public static void LogCondition(
        bool condition,
        string message = "",
        [CallerArgumentExpression(nameof(condition))] string expression = "")
    {
        if (!condition)
        {
            Console.WriteLine($"[FAILURE] Condition '{expression}' evaluated to false. Details: {message}");
        }
        else
        {
            Console.WriteLine($"[SUCCESS] Condition '{expression}' evaluated to true.");
        }
    }
}

When invoking this method:

int userAge = 16;
int minimumAge = 18;

DiagnosticLogger.LogCondition(userAge >= minimumAge, "User is too young.");

The output generated is:

[FAILURE] Condition 'userAge >= minimumAge' evaluated to false. Details: User is too young.

The compiler automatically substituted "userAge >= minimumAge" into the expression parameter at the call site, eliminating the need to pass that text manually.

Standard .NET Use Cases

The .NET runtime uses this attribute extensively in built-in validation methods. A primary example is ArgumentNullException.ThrowIfNull:

ArgumentNullException.ThrowIfNull(myService);

Behind the scenes, the framework signature utilizes the attribute:

public static void ThrowIfNull(
    [NotNull] object? argument, 
    [CallerArgumentExpression(nameof(argument))] string? paramName = null);

If myService is null, the exception automatically logs "myService" as the parameter name without requiring nameof(myService) from the caller.

Key Benefits

  • Zero Runtime Overhead: Expression capturing occurs at compile time, meaning no reflection or parsing cost is incurred during execution.
  • Refactoring Safety: Renaming variables or altering logical conditions automatically updates the logged expression during the next build.
  • Cleaner APIs: Methods require fewer manual string parameters, reducing code verbosity and keeping call sites concise.