Caller Information Attributes · How it works
4 min readHow it works
The four attributes
| Attribute | Parameter type | Value supplied | Since |
|---|---|---|---|
[CallerMemberName] | string | name of the calling method, property or event | C# 5 |
[CallerFilePath] | string | full path of the caller's source file at compile time | C# 5 |
[CallerLineNumber] | int | line number of the call | C# 5 |
[CallerArgumentExpression("p")] | string | the source text passed for parameter p | C# 10 |
A: Attributes from System.Runtime.CompilerServices placed on optional parameters:
public static void Trace(
string message,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0)
=> Console.WriteLine($"{Path.GetFileName(file)}:{line} {member}: {message}");
void PlaceOrder() => Trace("starting");
// OrderService.cs:42 PlaceOrder: startingA: At each call where the caller omits the argument, the compiler substitutes a literal: Trace("starting") compiles exactly as if you had written Trace("starting", "PlaceOrder", @"C:\src\Shop\OrderService.cs", 42). Nothing happens at runtime: no reflection, no stack walk, no allocation (the strings are interned literals in the caller's assembly). That is why the parameters must be optional (they need a default for the compiler to replace) and why the values describe the source code, not the runtime call stack.
new StackTrace() or MethodBase.GetCurrentMethod()?A: A stack walk is slow (microseconds, allocations) and unreliable: the JIT can inline a method so its frame disappears, and release builds may have no file or line information without PDBs. Caller info attributes are free and always accurate to the source, because the compiler writes the values in.
Common uses
[CallerMemberName] simplify INotifyPropertyChanged?A: The property setter can raise the event without typing its own name, so renaming the property cannot leave a stale string behind.
public class OrderViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _status = "";
public string Status
{
get => _status;
set => SetField(ref _status, value); // member name = "Status"
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
return true;
}
}[CallerArgumentExpression] do, and where is it used in .NET?A: It gives a method the source text of an argument, so error messages can show what the caller wrote. The BCL guard helpers use it:
ArgumentNullException.ThrowIfNull(order.Customer);
// ArgumentNullException: Value cannot be null. (Parameter 'order.Customer')
public static void ThrowIfNull(
object? argument,
[CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument is null) throw new ArgumentNullException(paramName);
}
Other examples: ArgumentException.ThrowIfNullOrEmpty, ArgumentOutOfRangeException.ThrowIfNegative (.NET 8), and assertion libraries that print Expected x.Count > 0 instead of Expected true. It is a C# 10 feature; the string is exactly the text between the call's parentheses for that argument, whitespace-normalized.
A: Logging and tracing helpers (Log.Debug("…") recording method, file and line), metrics and diagnostics wrappers (timing a block and tagging it with the member name), test frameworks (reporting the source line of a failed assertion), and source generators or mocks that need a stable key per call site.
What value do you get?
- method → its name (
"PlaceOrder"); - property or indexer accessor → the property name (
"Status","Item"for indexers); - constructor →
".ctor"; static constructor →".cctor"; finalizer →"Finalize"; - operator or conversion → its metadata name (
"op_Addition","op_Implicit"); - event accessor → the event name;
- lambda or local function → the name of the enclosing member, not the lambda;
- attribute argument on a member → the name of that member;
- field initializer → the field name.
[CallerMemberName] return for different kinds of callers?A:
A: The explicit value wins; the compiler only fills in values the caller omitted. Trace("x", "Custom") logs "Custom". This lets wrappers pass the information through.
Traps
A: Because caller info describes the immediate caller. If Log.Info(msg) calls Trace(msg) without forwarding, every entry shows "Info" from the wrapper's own file. Each layer must declare the same attributes and pass them on:
public static void Info(string msg,
[CallerMemberName] string member = "",
[CallerFilePath] string file = "",
[CallerLineNumber] int line = 0)
=> Trace(msg, member, file, line); // forward explicitly[CallerFilePath] a privacy and reproducibility concern?A: It embeds the absolute path on the build machine (for example C:\Users\jane\src\Shop\OrderService.cs) as a string constant in the compiled assembly, visible to anyone who decompiles it or reads the logs, and it makes builds from different folders produce different binaries. Map the source root with <PathMap>$(MSBuildProjectDirectory)=/src</PathMap> or build with ContinuousIntegrationBuild=true (deterministic builds, which Source Link setups enable), and log only Path.GetFileName(file).
params or required parameters?A: The attributed parameters must be optional (have a default value), and optional parameters must come after required ones. A method with params cannot put optional caller parameters after it, so logging helpers that take format arguments usually use an interpolated string handler or overloads instead.
dynamic or reflection calls?A: No. They are a compile-time feature of a statically bound call. When a method is invoked through dynamic, reflection (MethodInfo.Invoke) or a delegate created from it, the parameters simply get their declared defaults ("", 0) unless the invoker supplies values.