C# Interceptors (.NET 8): What They Are and How to Use Them
/ 5 min read
Updated:Found this post helpful?
Buy me a coffeeTable of Contents
C# Interceptors are a compiler feature introduced in .NET 8 (C# 12) that let a generated method body replace a specific method call at a specific call site, without touching the original source. As the name implies, Interceptors allow developers to target specific method invocations and intercept them with a new implementation, which is exactly the kind of swap I’ve wanted in .NET for a long time, something close to monkey patching. Interceptors have several purposes and meaningful distinctions that we’ll get into in this post. So let’s get started.
Updated October 2026: this post was written when interceptors were a .NET 8 preview-only feature behind the InterceptorsPreview language feature flag. The mechanics below (the InterceptsLocationAttribute, the need for exact file path/line/character matches) are still how interceptors work today, but the feature has since moved out of preview and is used by built-in source generators, including ASP.NET Core’s request delegate generator and EF Core’s precompiled queries. It’s still not meant for hand-written application code, and newer SDKs restrict which namespaces can emit interceptors via the InterceptorsNamespaces project property rather than a blanket preview switch. Check the current C# language reference or your SDK’s release notes before relying on the exact project settings shown here.
What is an Interceptor?
In .NET 8 preview 6, the SDK introduces additional functionality to “intercept” any method call within your codebase. The word “interceptor” is clear as to the purpose of this new functionality. It only replaces methods intentionally and does not replace method implementations globally. The approach means you, as the developer, must be systematic about using an Interceptor.
The .NET team uses interceptors to overwrite infrastructure code that previously relied on reflection with compile-time versions specific to your application. Interceptors will hopefully reduce your programs’ start-up time and efficiency. The .NET team designed interceptors to work with source generators, as source generators can surgically deal with abstract syntax trees and code files to target method invocations. While you could hand-write interceptor calls, it becomes impractical in a real-world application.
Let’s get into setting up your project to work with interceptors.
Getting Started with Interceptors
Interceptors are a .NET 8 preview 6 feature, so you’ll need the matching SDK version or higher to get this working. Begin by creating a new console application, or really any .NET application.
Next, in your .csproj, you must add the following PropertyGroup element.
<PropertyGroup> <Features>InterceptorsPreview</Features></PropertyGroup>Also be sure to set your LangVersion element to preview to get access to the feature.
<PropertyGroup> <LangVersion>preview</LangVersion></PropertyGroup>Next, add the following attribute definition to your project.
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]public sealed class InterceptsLocationAttribute : Attribute{ public InterceptsLocationAttribute(string filePath, int line, int character) { }}Yes, it’s weird that the attribute isn’t part of the BCL, but since this is a preview feature, I imagine the .NET team didn’t want to pollute the .NET framework with a potential API change later.
You’ll notice that the attribute takes three parameters: filePath, line, and character. You’ll also note that these values aren’t assigned anywhere, and you’d be correct. The attribute is a marker the compiler will read at compile-time, so setting the values for runtime use is pointless.
Now, let’s intercept some code. Add the following to your Program.cs file. Note, the line numbers and spacing are critically important. If you reformat the code, this solution might break. Also be sure to change the file path to the absolute path of your Program.cs file.
using System.Runtime.CompilerServices;
C.M(); // What the Fudge?!C.M(); // Original
class C{ public static void M() => Console.WriteLine("Original");}
// generatedclass D{ [InterceptsLocation("/Users/khalidabuhakmeh/RiderProjects/ConsoleApp12/ConsoleApp12/Program.cs", line: 3, character: 3)] public static void M() => Console.WriteLine("What the Fudge?!");}Running the application above, you’ll see the most peculiar thing. Two different outputs from the same method call! What the heck?!
What the Fudge?!OriginalBut how? What does the compiled code look like after compilation? We can see what happened using JetBrains Rider’s IL Viewer.
// Decompiled with JetBrains decompiler// Type: Program// Assembly: ConsoleApp12, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null// MVID: 09D7E1E0-5709-4A62-884A-AB84DAA1E08C// Assembly location: /Users/khalidabuhakmeh/RiderProjects/ConsoleApp12/ConsoleApp12/bin/Debug/net8.0/ConsoleApp12.dll// Local variable names from /users/khalidabuhakmeh/riderprojects/consoleapp12/consoleapp12/bin/debug/net8.0/consoleapp12.pdb// Compiler-generated code is shown
using System.Runtime.CompilerServices;
[CompilerGenerated]internal class Program{ private static void <Main>$(string[] args) { D.M(); C.M(); }
public Program() { base..ctor(); }}We can now see that the compiler replaced the first method invocation with our intercepting implementation. Wow!
After the razzle-dazzle wears off, you’ll likely think this is impractical. Who has time to hardcode full paths to files, count lines, and count columns? Well, as mentioned previously, this is where source generators come in.
While I won’t demonstrate it here when dealing with syntax trees, you do have access to information like FilePath, and every CSharpSyntaxNode has a GetLocation method that gives you access to line numbers and location within a code file. If you’re already proficient in writing source generators, this information is already available.
Conclusion
This feature is definitely for a select group of individuals in the .NET community, specifically those who write and maintain source generators. Within that subgroup, you likely have framework authors looking to squeeze every last bit of performance out of .NET. As you’ve seen, Interceptors only change specific implementations and cannot target methods globally. If you’re using a source generator to do an interception of all methods, you’ll have to generate an interception call for each location. Generating a large amount of custom code can adversely affect the size of your compiled assets, so be aware of using this feature. Also, you may consider avoiding this feature altogether. Interceptors are still in preview, and the primary intention is to help .NET authors to improve ASP.NET Core and other frameworks within the .NET SDK. Either way, it’s good to understand this feature exists next time you’re debugging your .NET 8 applications because the method you think you’re calling might not be the method you’re actually calling.
I hope you enjoyed this blog post, and as always, thanks for reading and sharing my posts with friends and colleagues.