In C# 12, interceptors are a new experimental compiler feature that enables rerouting specific method calls to different code.
Typically, developers use interceptors with code generators to modify the behavior of existing code without altering the code itself. In this article, we will demonstrate how to introduce interceptors into our code and explore the use cases for this feature.
Let’s get started.
Introducing Interceptors Into Our Code
To introduce interceptors into our application, we need to:
- Enable the interceptors feature
- Declare the
InterceptsLocationAttribute
class - Create a static class that will include the interceptor method
- Add the
InterceptsLocationAttribute
attribute to the interceptor method - Introduce a prebuild command to our
.csproj
file
Enabling the interceptors feature
Let’s start by creating a simple console application. To enable the interceptors feature, we have to explicitly add the feature flag in our .csproj
file by adding <LangVersion>preview</LangVersion>
and <Features>InterceptorsPreview</Features>
under the PropertyGroup
tag:
<PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>preview</LangVersion> <Features>InterceptorsPreview</Features> </PropertyGroup>
In our console application, let’s first create an Example
class. This class has a GetText()
method that accepts a string and returns a string. The GetText()
method will represent the method that we want to intercept in our application so that it returns a different string:
public class Example { public string GetText(string text) { return $"{text}, World!"; } }
Then, in our Program
class, let’s create an instance of the Example
class, call the GetText()
method and finally print the result. Once we run our application, the console will print “Hello, World!” as expected:
var example = new Example(); var text = example.GetText("Hello"); Console.WriteLine(text); //Hello, World!
Declaring the InterceptsLocationAttribute in the Project
The InterceptsLocationAttribute
class is a C# 12 attribute designed to label methods as interceptors. However, as of the publication of this article, interceptors are still in preview, and the attribute hasn’t yet been included in .NET 8. Consequently, we need to declare it manually within our project under the System.Runtime.CompilerService
namespace:
namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method)] public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute { }
The InterceptsLocationAttribute
class is an attribute that targets methods exclusively and accepts three arguments, which are the file path, line number, and the starting character column number of the method we want to intercept.
Also, we did not assign the file path, line number, and the starting character column number anywhere since the attribute serves as a marker that the compiler reads during compilation, making it irrelevant to set values for runtime use.
Creating the Interceptor Method
Next, let’s create a static GeneratedCode
class that will include the interceptor method to which our call will be rerouted. The interceptor method should be declared as an extension method because it will replace the original method in the Example
class:
namespace InterceptorsInCSharp; public static class GeneratedCode { public static string InterceptorMethod(this Example example, string text) { return $"{text}, Code Maze"; } }
Note that the interceptor method must have the exact same signature as the intercepted method; otherwise, when we run the application, it will throw a runtime exception.
Adding the InterceptsLocation attribute to the Interceptor Method
The next step is to import the InterceptsLocationAttribute
class from the System.Runtime.CompilerServices
namespace and add the attribute to the interceptor method. Here we provide the absolute path to the Program.cs
file, the line number, and the starting character column number of our GetText()
call:
using System.Runtime.CompilerServices; internal static class GeneratedCode { [InterceptsLocation("absolute/path/to/program.cs", 9, 28)] public static string InterceptorMethod(this Example example, string param) { return $"{param}, Code Maze"; } } }
The string 'absolute/path/to/program.cs'
will throw a compilation error since it is not the correct absolute path. We will discuss how to solve this issue in the next section.
Note that to get the line number and the starting character column number, we can use Visual Studio by placing the cursor before the starting character of the GetText()
method and checking the bottom right corner of the editor window (check the image below for reference):
Introducing the Prebuild Command to Our .csproj File
At this point, if we try to build our project, the build will fail, and the compiler will throw this compilation error:
CS9139 Cannot intercept: compilation does not contain a file with path 'absolute/path/to/program.cs'
That is because the compiler does not recognize the string 'absolute/path/to/program.cs'
as a valid path to the program.cs
file.
One way to solve this issue is to hardcode the absolute path in the interceptor attribute. However, this might cause an issue when compiling the application on another machine since the absolute path to the program.cs
file would be different.
A better solution would be to add a prebuild event to our project, which replaces the invalid path with the correct one before compiling the application. This event will run a PowerShell script that takes care of the string replacement.
First, we have to make sure that we install the cross-platform version of PowerShell on our machine. PowerShell supports all major platforms such as Windows, Linux, and MacOS.
Second, we need to add a PowerShell script pre-build-event.ps1
to our project folder:
$curPath = Get-Location $progCsPath = Join-Path -Path $curPath -ChildPath "Program.cs" (Get-Content GeneratedCode.cs) | ForEach-Object { $_.Replace('"absolute/path/to/program.cs"', "@""${progCsPath}""")} | Set-Content GeneratedCode.cs
The purpose of this script is to replace all the 'absolute/path/to/program.cs'
strings by the actual Program.cs
file path in the GeneratedCode.cs
file.
Then, we can add the prebuild command to our .csproj
file:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <LangVersion>preview</LangVersion> <Features>InterceptorsPreview</Features> </PropertyGroup> <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> <Exec Command="pwsh pre-build-event.ps1" /> </Target> </Project>
Once we build our code, we will see that the string 'absolute/path/to/program.cs'
is replaced by the actual absolute path of the program.cs
file.
Now, if we run the application, the compiler will reroute the call to our interceptor method, and it will print “Hello, Code Maze!” instead of “Hello, World!”.
Allow Intercepting Multiple Calls
We can even add multiple InterceptsLocation
attributes to the interceptor method to intercept multiple calls. We need to set the AllowMultiple
attribute of the InterceptsLocationAttribute
class to true
:
namespace System.Runtime.CompilerServices; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute { }
Let’s add another call to the GetText()
method in the Example
class and print the result. At this point, if we run the application, the second call will return “Greetings, World!” as expected:
var example = new Example(); var text = example.GetText("Hello"); var anotherText = example.GetText("Greetings"); Console.WriteLine(text); // Hello, Code Maze! Console.WriteLine(anotherText); // Greetings, World!
Next, let’s add another InterceptsLocation
attribute to the InterceptorMethod()
extension method with the correct file path, line, and character of the second call. Note that the prebuild event will replace all 'absolute/path/to/program.cs'
strings in the file:
public static class GeneratedCode { [InterceptsLocation("absolute/path/to/program.cs", 9, 28)] [InterceptsLocation("absolute/path/to/program.cs", 10, 35)] public static string InterceptorMethod(this Example example, string param) { return $"{param}, Code Maze"; } }
If we run the application, the output will appear as follows:
Hello, Code Maze! Greetings, Code Maze!
When to Use Interceptors
Interceptors are a powerful tool that we can use to automate tasks. This can help to improve the quality, maintainability, and performance of our code.
We can use the interceptors feature to automatically log method calls and their parameters. This helps in tracking down bugs or auditing user activity. Another use for interceptors is to cache the results of method calls, which improves the overall performance of our application. We can also use interceptors to collect metrics about method calls such as how many times we call the method, the call duration, and the exceptions that occur during execution. Another use case of interceptors is to mock our method calls, which can be useful in unit and integration testing.
Conclusion
With the release of C# 12, we can see that Microsoft is clearly trying to boost the ahead-of-time (AOT) compilation in C# applications. And with features like interceptors, overriding legacy code will become easier for developers.
However, interceptors are an experimental feature that might be changed or removed in later stages and thus should not be used for production. You can click here to learn more about C# 12’s new features.