C# InterceptsLocation Attribute Usage
Learn how to use C# InterceptsLocationAttribute with source generators to redirect method calls at compile time, including syntax, limitations, and debugging tips.
The InterceptsLocationAttribute is a compiler-recognized attribute used with C# 12 (and later) when working with source generators. It tells the compiler that a particular method is an interceptor and provides the exact location (file path, line, and character) of the call that should be redirected to this interceptor method. This capability is part of an experimental feature that enables compile-time interception of method calls—meaning you can replace an invocation with a different implementation without modifying the original source code.
How Interceptors Work in C#
Interceptors are a compile-time mechanism. When the C# compiler encounters a call that matches an interceptor, it emits a call to the interceptor method instead of the original target. The mapping is based on the InterceptsLocationAttribute applied to the interceptor method, which specifies the exact location of the call to intercept. This location consists of the file path, line number, and character position. The compiler uses this information to substitute the call during compilation.
For example, consider a simple call:
// Original code file: Program.cs class Program { static void Main() { Console.WriteLine("Hello"); } }
An interceptor can be defined elsewhere (likely generated by a source generator) that substitutes the Console.WriteLine call. The interceptor method is marked with InterceptsLocationAttribute:
// Generated interceptor file using System.Runtime.CompilerServices; static class Interceptors { [InterceptsLocation("Program.cs", 6, 9)] public static void InterceptWriteLine(string message) { Console.WriteLine($"Intercepted: {message}"); } }
In this example, the attribute specifies the path Program.cs, line 6, and character 9—the position where the Console.WriteLine call starts. When the source generator produces this interceptor, the compiler rewrites the call to InterceptWriteLine. The original code remains unchanged, but the runtime behavior is altered.
Syntax and Placement of the Attribute
InterceptsLocationAttribute is defined in the System.Runtime.CompilerServices namespace. It requires one string argument (the file path) and two integer arguments (line and character position). The file path must match the path as seen by the compiler, which is typically the full path or a relative path depending on how the project is structured. The line and character are 1-based; line 1 is the first line, and character 1 is the first column.
The attribute can only be applied to static methods. The interceptor method must have a signature that is compatible with the call site—same return type and a parameter list that matches the arguments in the intercepted call. If the signature does not match, the compiler will report an error.
Minimal Example with a Source Generator
To use interceptors in practice, you typically pair them with a source generator that analyzes the code and emits interceptor methods. A minimal source generator might look like this:
// Generator code (C#) using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System.Text; [Generator] public class InterceptorGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterSourceOutput(context.CompilationProvider, (spc, compilation) => { // Assume we find calls to Console.WriteLine in the compilation. // For simplicity, hardcode the location. string source = @" using System.Runtime.CompilerServices; static class Interceptors { [InterceptsLocation(""Program.cs"", 6, 9)] public static void InterceptWriteLine(string message) { Console.WriteLine($""Generated: {message}""); } }"; spc.AddSource("Interceptors.g.cs", SourceText.From(source, Encoding.UTF8)); }); } }
This generator emits a static class with the interceptor method. However, the location is hardcoded. A real generator should traverse the syntax tree, locate the invocation to intercept, and read location.GetLineSpan(). That returns Path, StartLinePosition.Line, and StartLinePosition.Character. Because InterceptsLocationAttribute uses 1-based line and character positions, add one to the Roslyn line and character values before embedding them in the generated source. This is how an interceptor correctly targets the intended call.
Runtime Cost and Performance Considerations
Interceptors have almost no runtime cost because the substitution happens at compile time. The generated code is directly compiled into the assembly, so there is no reflection or runtime lookup. The performance impact is equivalent to writing the interceptor call directly in the source. However, there is a build-time cost: the source generator must analyze the compilation and emit the interceptor code. For large codebases, this adds to compilation time. Also, because interception is resolved at compile time, the original method's implementation is never executed; this can change behavior if the original method had side effects that the interceptor does not replicate.
Limitations and Compatibility
Interceptors are an experimental feature. They require C# 12 and the .NET 8 SDK (or newer). To enable them, you must set the <InterceptorsPreview> property to true in your project file:
<PropertyGroup> <LangVersion>preview</LangVersion> <InterceptorsPreview>true</InterceptorsPreview> </PropertyGroup>
Without this setting, the compiler does not apply interceptors. Because the feature is experimental, the API could change in future releases. The attribute is not intended for general use; it is primarily designed for source generator authors who want to enable incremental code modifications (like logging or caching) without changing the original code. For most applications, you will not use interceptors directly unless you are building a library that relies on this behavior.
Common Mistakes and Debugging Tips
One common mistake is specifying the wrong file path, line, or character. The path must match the value reported by the compiler's syntax tree (SyntaxTree.FilePath), and line and character positions are 1-based. Use GetLocation() from a Roslyn syntax node to get the precise values. Another mistake is applying the attribute to non-static methods; this results in a compiler error. The interceptor method must also be visible from the call site, so put generated interceptors in an internal or public static class rather than a private nested location.
To debug interceptors, you can inspect the generated code by adding the EmitCompilerGeneratedFiles property to your project file. This saves the generated source to disk, allowing you to verify the attribute values. If interception does not happen, check that the compiler flag is enabled and that the attribute location matches exactly. Use the Roslyn syntax tree to print the line and character values for the call node to confirm.
When to Use Interceptors
Interceptors are a powerful tool for library authors who need to augment existing code without modifying it. They are especially useful for scenarios like implicit logging, caching, or observability, where you want to automate changes across many call sites. However, they are not a general-purpose code modification tool. Because they rely on exact source locations, they are sensitive to code changes; any edit that shifts a line or character will break the interceptor. Therefore, they should be used in controlled environments where the source code is stable, or in conjunction with a source generator that automatically updates the locations during build. For most application code, simpler approaches like dependency injection or reflection are preferable.
Production Considerations
When using interceptors in a production codebase, consider the maintainability impact. The generated interceptor code is separate from the original source, which can make debugging harder. Also, because the interception is invisible—the original call appears unchanged in the source—developers might be confused when the behavior differs. Ensure that your source generator is well-documented and that the interception is transparent. Additionally, test the build in a continuous integration environment to ensure the compiler setting is present. If you upgrade the .NET version, verify that the interceptor feature is still supported or check for changes. Since this is experimental, keep an eye on official documentation and adjust accordingly.