C# CallerLineNumber: Usage and Examples
Learn how to use the C# CallerLineNumber attribute to capture the exact source line of method calls for logging, debugging, and diagnostics.
The C# CallerLineNumber attribute is one of the caller info attributes. When applied to an optional integer parameter, it lets the compiler insert the source line number at each call site. That makes it useful for logging, assertions, and diagnostics when you need to know exactly where a method was called from without parsing a stack trace.
What Are the Caller Info Attributes?
The caller info attributes are CallerMemberName, CallerFilePath, and CallerLineNumber. They are defined in the System.Runtime.CompilerServices namespace and can be applied to optional parameters. When a method is called, the compiler fills in the appropriate value at the call site. This happens at compile time, so there is no runtime cost for retrieving the information.
using System.Runtime.CompilerServices; public void Log(string message, [CallerMemberName] string member = "", [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) { Console.WriteLine($"{file}:{line} {member}: {message}"); }
In this example, calling Log("Something happened") from a method causes the compiler to fill in the caller’s member name, file path, and line number automatically. You do not need to pass them explicitly.
Using CallerLineNumber in a Method
The CallerLineNumber attribute is applied to an optional integer parameter. The compiler replaces the default value with the line number of the call statement. This is useful when you want to log the exact source location where a diagnostic event occurred.
public void Trace(string message, [CallerLineNumber] int line = 0) { Console.WriteLine($"Line {line}: {message}"); }
Calling Trace("Request started") from line 42 of a file will output Line 42: Request started. The value is computed by the compiler, so it reflects the location in the source code, not the runtime execution point.
Practical Use Cases for CallerLineNumber
A common use case is logging frameworks. When you log an error, you often want to know where the log call originated. Instead of manually passing a line number or using a stack trace, you can use CallerLineNumber to capture it automatically.
public void Error(string message, [CallerMemberName] string member = "", [CallerLineNumber] int line = 0) { LogToFile($"ERROR at {member} (line {line}): {message}"); }
Another use case is debugging assertions. A custom assertion method can report the failing line number:
public void Assert(bool condition, [CallerLineNumber] int line = 0) { if (!condition) { throw new InvalidOperationException($"Assertion failed at line {line}"); } }
How the Compiler Resolves Caller Info
The compiler substitutes the values at the call site. When you write a call like Log("test"), the compiler sees the optional parameters with caller info attributes and inserts the current file path, line number, and member name as arguments. The resulting IL contains the literal values, so using the values costs no stack walk at runtime.
The substitution happens only for direct calls that the compiler can bind at compile time. It is not a replacement for StackTrace when you need to reconstruct a call stack or when the call site is known only at runtime, such as with reflection or dynamic dispatch.
Limitations and Compatibility Considerations
The caller info attributes are available starting in C# 5.0. They are included in .NET Framework 4.5 and later, and in modern .NET versions. If you target an older framework, you can define a matching System.Runtime.CompilerServices.CallerLineNumberAttribute in your project; the compiler recognizes it by its full name.
One limitation is that the attribute can be applied only to optional parameters. If a caller passes an explicit argument for the parameter, that argument is used instead of the compiler-generated value.
The line number reported is the line where the method call appears, not the line where the method body starts. For a multi-line call, it points to the first line of the call expression. Calls inside lambdas or local functions still report the source location of the call.
If you obfuscate code, remember that CallerFilePath and CallerMemberName values are emitted as string literals at compile time. They may not match renamed methods or the original source path after obfuscation or a build-server copy.
Comparing CallerLineNumber with StackTrace
Before caller info attributes, developers often used StackTrace to get the calling method and line number. That approach has several drawbacks: it is slower, may require access to symbol/PDB information for line numbers, and can be less accurate in optimized builds. In contrast, CallerLineNumber is a compile-time literal, so it has no runtime overhead and is accurate for the call site.
| Approach | Runtime Cost | Accuracy | Ease of Use |
|---|---|---|---|
| CallerLineNumber | None | Exact | High |
| StackTrace | High | Depends | Low |
Use CallerLineNumber when you need a lightweight, reliable way to capture the source line. Use StackTrace only when you need the full call stack or when the call site cannot be known at compile time.
Best Practices for Maintainable Diagnostics
Keep the parameters optional and provide sensible defaults. This ensures that existing callers do not break when you add the parameters. Also, when using caller info attributes in a library, remember that the values describe the library’s caller, not the library’s own code.
For logging, keep the method thin so the line number reflects the original call site. If a logging method calls another logging method that also uses CallerLineNumber, the value in the inner method would point to the call inside the outer method, not to the original caller. Pass the caller info values explicitly if you must delegate, or write directly to the log inside the method decorated with the attributes.
public void LogInfo(string message, [CallerMemberName] string member = "", [CallerLineNumber] int line = 0) { // Directly write to the log; do not delegate to another caller-info method WriteToLog($"{member}:{line} - {message}"); }
Use the attributes consistently across logging and assertion helpers. This gives a uniform way to trace issues back to the exact source line, especially in codebases where multiple developers contribute to the same files.
Finally, CallerFilePath returns the full path used at compile time. On a build server, that path may point to a temporary directory. Consider using only the file name or a path relative to the project root to keep logs readable.