C# Interceptors: Modifying Method Calls at Build Time
Interceptors are an experimental compiler feature introduced in C# 12 and .NET 8 that allow source generators to reroute specific method calls to alternative code at compile time. This article explains what interceptors are, how they manipulate the Abstract Syntax Tree (AST) during compilation to replace call sites, how they differ from runtime patching, and why they are essential for optimizing Native Ahead-of-Time (AOT) compilation and runtime performance.
What Are C# Interceptors?
An interceptor is a method that can declaratively substitute a call to another method at a specific source code location during compilation. Unlike traditional interception techniques—such as runtime reflection, dynamic proxies (e.g., Castle DynamicProxy), or Intermediate Language (IL) weaving (e.g., Fody)—C# interceptors execute entirely within the Roslyn compiler pipeline.
When you compile an application using interceptors, the compiler rewires the call site directly in the emitted IL code. The original method remains untouched, but the specific call location is permanently bound to the interceptor method instead. Because this occurs at build time, there is zero runtime overhead, no dynamic dispatch penalty, and complete compatibility with Native AOT environments where dynamic code generation is prohibited.
How Interceptors Modify Method Calls at Build Time
The build-time modification process relies on collaboration between the Roslyn compiler and source generators. The workflow operates as follows:
- Source Parsing and Call Site Identification: The Roslyn compiler parses the source code into a syntax tree. A Roslyn incremental generator scans the syntax tree to identify specific method invocations that qualify for interception (for example, methods utilizing reflection or complex generic dispatch).
- Location Mapping: The generator records the exact physical location of the call site, including the relative file path, line number, and character offset within the source file.
- Code Generation: The generator creates a new C#
source file containing the interceptor method. This method is decorated
with the
InterceptsLocationAttribute, passing the exact file path, line, and character coordinates of the target call site. - AST Rewrite and IL Emission: During the compilation
phase, when the compiler encounters an invocation at the coordinates
specified by the
InterceptsLocationAttribute, it substitutes the invocation target. Instead of emitting an ILcallorcallvirtinstruction to the original method, it emits the instruction pointing directly to the generated interceptor method.
Mechanics and Syntax
To use interceptors, the feature must be enabled in the project file, and the compiler must be instructed which namespaces are permitted to contain interceptors:
<PropertyGroup>
<Features>InterceptorsPreview</Features>
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);MySourceGeneratorNamespace</InterceptorsPreviewNamespaces>
</PropertyGroup>The compiler requires the declaration of the
InterceptsLocationAttribute within the
System.Runtime.CompilerServices namespace:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(string filePath, int line, int character)
{
}
}
}An interceptor method must match the signature, return type, and
generic constraints of the target method. If intercepting an instance
method, the interceptor must be defined as an extension method or static
method where the first parameter represents this:
namespace MySourceGeneratorNamespace
{
public static class GeneratedInterceptors
{
// Intercepts a call at Program.cs, line 10, character 5
[System.Runtime.CompilerServices.InterceptsLocation("Program.cs", line: 10, character: 5)]
public static void InterceptedLog(string message)
{
// Specialized, high-performance logic replaces the original call
Console.WriteLine($"[Optimized]: {message}");
}
}
}Primary Use Cases
- Ahead-of-Time (AOT) Compilation: Frameworks like ASP.NET Core often rely on reflection to analyze route handlers and bind parameters. Interceptors allow source generators to inspect minimal API routes at build time and replace dynamic mapping calls with hardcoded, strongly-typed serializers and binders.
- Performance Optimization: General-purpose library methods can be replaced with specialized versions tailored to the exact compile-time constants passed at a specific call site.
- Logging and Diagnostics: Diagnostic tools can inject tracing or telemetry hooks into specific calls without modifying source code directly or using IL post-processors.