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:
- 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.
- Context-Rich Debug Logging: It logs both the evaluation result and the expression itself, giving developers immediate context on why a branch was executed.
- 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.