C# InterceptsLocation Attribute Usage
c# interceptslocation attribute: Learn how to use InterceptsLocationAttribute to replace method calls at compile time with source generators and interceptors.
The c# interceptslocation attribute, formally 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. Non-static methods are not allowed. The interceptor method must have a compatible signature with the original method—same return type and parameter list (including optional parameters). 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, notice that the location is hardcoded—a real generator would use syntax trees to locate the exact call node. The generator would traverse the syntax tree, find an invocation expression of interest, and obtain its GetLocation() details: location.GetLineSpan() gives the path, start line, and start character. Those values are then embedded into the generated attribute. This is how the 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 ignores the attribute. Also, the attribute is not available in older .NET frameworks; you must target .NET 8 or later. Because it is experimental, the API could change in future releases. The attribute is not intended for general use; it's 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 or character offset. If the path does not match exactly (including case sensitivity and slash direction), the compiler will not recognize the interceptor. Use the GetLocation() method from the SyntaxNode to get the precise values. Another mistake is applying the attribute to non-static methods; this results in a compiler error. Also, the interceptor method must be visible from the call site—if it's private, the compiler might still allow it because the attribute is recognized at compile time, but it's safer to make it public or internal.
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 the interception doesn't happen, check that the property is enabled and that the attribute's 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.