Back to Blog
C#

C# Caller Line Number: Usage and Examples

c# caller line number: Learn how to use the C# caller line number attribute to capture the exact source location of method calls for logging, debugging, and diagnostics.

C#CallerInfoLoggingDiagnosticsAttributes
C# caller line number attribute showing source code location in diagnostics

c# caller line number requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to know exactly where a method was called from, C# provides a set of caller info attributes that capture the file path, line number, and member name of the call site. The CallerLineNumber attribute is particularly useful for logging and diagnostics because it gives you the precise source line 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 injects 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, if you call Log("Something happened") from a method, the compiler fills in the caller's member name, file path, and line number automatically. You don't 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 actual 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 Caller Line Number

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 __LINE__ 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. You can create a custom assertion method that reports the failing line number:

public void Assert(bool condition, [CallerLineNumber] int line = 0) { if (!condition) { throw new InvalidOperationException($"Assertion failed at line {line}"); } }

This makes it easier to locate the exact source line that triggered the failure, especially in codebases where multiple assertions exist.

How the Compiler Resolves Caller Info

The compiler evaluates these attributes 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. This is a compile-time transformation, so the resulting IL contains the literal values.

Because the values are baked into the IL, they remain correct even if the code is later refactored, as long as the call site line numbers change accordingly. The attributes do not work in every context. For example, they are not supported in default parameter values of delegates or in dynamic invocations. Also, if the method is called from an expression tree, the compiler may not inject the values because the call is not a direct method call.

Limitations and Compatibility Considerations

The caller info attributes are available in C# 5.0 and later. They work with .NET Framework 4.5 and later, as well as all modern .NET versions. However, they do not work with older runtimes that do not support the CallerLineNumberAttribute type. If you are targeting an older framework, you can define the attribute yourself, but the compiler will still recognize it if it is named correctly.

One limitation is that the line number is the line where the method call appears, not the line where the method body starts. If you have a multi-line call, the line number points to the first line of the call expression. Also, if the call is inside a lambda or a local function, the line number still refers to the source location of the call.

Another consideration is obfuscation. If you obfuscate your code, the file paths and member names may be altered, but line numbers typically remain unchanged. This can still be useful for diagnostics, but you should be aware that the file path might not match the original source layout.

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 slow, requires permission to access stack information, and can be inaccurate in optimized builds where line numbers are not preserved. In contrast, CallerLineNumber is a compile-time constant, so it has zero runtime overhead and is always accurate for the call site.

ApproachRuntime CostAccuracyEase of Use
CallerLineNumberNoneExactHigh
StackTraceHighDependsLow

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 you are dealing with dynamically generated code where the attributes are not available.

Best Practices for Maintainable Diagnostics

When using caller info attributes, keep the parameters optional and provide sensible defaults. This ensures that existing callers do not break when you add the parameters. Also, avoid using these attributes in public API methods that are part of a library contract, because the injected values depend on the caller's source, which may not be meaningful for consumers of the library.

For logging, consider wrapping the attributes in a dedicated logging method so that the line number reflects the original call site, not the logging helper. For example, if you have a Log method that itself calls another logging method, the CallerLineNumber in the inner method would point to the call inside Log, not the original caller. To propagate the original location, pass the values as arguments or use a single method that directly performs the logging.

public void LogInfo(string message, [CallerMemberName] string member = "", [CallerLineNumber] int line = 0) { // Directly write to the log, do not delegate to another method WriteToLog($"{member}:{line} - {message}"); }

If you need to delegate, you can pass the caller info parameters explicitly to the inner method, but that defeats the purpose of the attributes. A better approach is to keep the logging method thin and perform the actual I/O inside it.

Another best practice is to use these attributes consistently across your logging and assertion methods. This gives you a uniform way to trace issues back to the exact source line, which is especially valuable in large codebases where multiple developers contribute to the same files.

Finally, be aware that the file path returned by CallerFilePath is the full path at compile time. If you build on a build server, the path may point to a temporary directory. In that case, consider using only the file name or a relative path derived from the project root to keep logs readable.

c# caller line number: Practical Usage and Code Examples | RYUSLOG DEV