How C# 12 Interceptors Work at Compile Time

Interceptors in C# 12 are an experimental compiler feature that allows specific method calls to be rerouted to alternative code implementations at compile time. Primarily built to enhance Roslyn source generators, interceptors replace traditional runtime hooks, dynamic dispatch, or reflection with static, compile-time substitutions. This article explains how interceptors function, how the compiler maps call sites to replacement methods, and the architectural benefits they provide for ahead-of-time (AOT) compilation and runtime performance.

The Core Concept of Interceptors

An interceptor is a method capable of substituting a call to another method at a specific call site in source code during compilation. Instead of emitting Common Intermediate Language (CIL) that invokes the original method, the Roslyn compiler redirects the invocation directly to the interceptor method.

Unlike runtime interception techniques—such as dynamic proxies, IL weaving, or reflection—compile-time interception incurs zero runtime overhead. The invocation is resolved fully before the application executes.

How the Compiler Identifies Interceptions

Interceptors rely on exact source-code coordinates to map a replacement method to a target call site. This process involves three main components:

  1. Feature Activation: Because interceptors are an experimental feature in C# 12, they must be explicitly enabled in the project file (.csproj):

    <PropertyGroup>
      <Features>InterceptorsPreview</Features>
    </PropertyGroup>
  2. The InterceptsLocation Attribute: The compiler identifies interceptor methods through the InterceptsLocationAttribute declared within the System.Runtime.CompilerServices namespace. This attribute specifies the exact file path, line number, and character offset of the invocation to intercept:

    namespace System.Runtime.CompilerServices
    {
        [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
        public sealed class InterceptsLocationAttribute : Attribute
        {
            public InterceptsLocationAttribute(string filePath, int line, int character)
            {
            }
        }
    }
  3. Call-Site Resolution: During the compilation pipeline, Roslyn analyzes the abstract syntax tree (AST). When it encounters a method call whose file, line, and character position match the coordinates declared on an [InterceptsLocation] attribute, the compiler rewrites the call target in the emitted metadata to reference the interceptor.

Technical Requirements and Rules

For an interceptor to successfully replace a call site, the compiler enforces strict rules:

  • Signature Compatibility: The interceptor method must match the signature, return type, and parameter types of the intercepted method. For instance methods, the interceptor must be defined as an extension method or accept the instance as its first parameter (this).
  • Static Context: Interceptor methods must be static.
  • Exact Coordinates: The character position provided to InterceptsLocationAttribute must point to the beginning of the identifier for the method being invoked.
  • Internal or Public Visibility: Interceptors must reside in a location accessible to the compilation unit.

Practical Example

Consider a standard method invocation:

// Program.cs
var worker = new Worker();
worker.Execute(); // Suppose 'Execute' starts at line 3, character 8

A source generator produces the interceptor code to reroute this specific execution:

// GeneratedInterceptors.cs
namespace MySourceGenerator
{
    public static class WorkerInterceptors
    {
        [System.Runtime.CompilerServices.InterceptsLocation(
            filePath: @"C:\Project\Program.cs", 
            line: 3, 
            character: 8)]
        public static void InterceptExecute(this Worker worker)
        {
            Console.WriteLine("Intercepted: Optimized execution path.");
        }
    }
}

When compiled, any call to worker.Execute() at that designated location executes InterceptExecute instead.

Why Interceptors Rely on Source Generators

Manual authoring of interceptors is fragile because altering a source file—such as adding a line of code or renaming a file—breaks the line and character mappings. Consequently, interceptors are designed strictly as an implementation detail for Roslyn source generators.

Source generators run as part of compilation, allowing them to inspect code, calculate precise syntax token locations, and immediately emit corresponding InterceptsLocation attributes before the final binary is generated.

Primary Use Cases and Benefits

  • Native AOT Compatibility: Traditional frameworks rely on runtime reflection to inspect types and dynamically bind methods. Interceptors allow source generators to convert reflective patterns into hardcoded, statically analyzable calls suitable for Native AOT.
  • Framework Optimization: Frameworks such as ASP.NET Core use interceptors to transform Minimal API route handlers, mapping generic or object-based request delegates directly to strongly-typed internal handlers at compile time.
  • Zero Runtime Cost: Because dispatch is resolved at build time, there are no virtual method lookups, dictionary checks, or reflection overheads.