Interceptors were shipped as a preview/experimental feature as part of C#12. The purpose of the interceptor is to allow you to intercept/replace existing method calls within your application with a substitute method of your choosing.
When first hearing about interceptors, you might wonder why anyone would need this type of feature, especially, when you can use reflection or dependency injection to be something similar. The answer is simple, the difference is when the interception occurs Interceptions will be made at compile time rather than at run-time, so they will add a performance boost.
If you are a normal .NET dev, the question you will get answers to in this article is whether you will need to adopt this feature for your day-to-day development. If you want to learn everything you need to know in order to master this powerful new feature, read on 🔥🔥🔥
Intercetpors, AOT & .NET 8
Interceptors were first released as part of .NET 8 preview 6 and in my opinion, it's not immediately obvious why you might need to use this feature. An interceptor will allow you to hijack calls to other methods at compile time rather than run time. For normal applications, if you need to swap out the functionality of a method, you will likely be sticking with dependency injection and some sort of design pattern at run-time, so who is this feature aimed at?
Currently, interceptors will be most useful for developers who create source generators. In case you were not aware the .NET source generator feature was introduced in .NET 5 and C# 9. Source generators allow developers to create and manipulate C# source code during compilation. Some examples of source generators include:
- AutoMapper.SourceGenerator: Generates mapping code during compilation
- RoslynDiagnostics.SourceGenerator: Helps to enforce coding standards and detect issues in their codebase during compilation
- GraphQL.SourceGenerator: Generates strongly types models at compilation
- Entity Framework Core Source Generator: Generate strong types models at compilation
So assuming you aren't a source generator developer, why has Microsoft added this feature? The answer to that lies in some other improvements made within .NET 7 and 8...
Within .NET 8 there is a new mode that you can leverage called Ahead-of-time (AOT) compilation. In normal .NET, when you compile code it is not directly converted into assembly (the language the processor can understand). Instead, it is first converted into something called Intermediate Language (IL). This IL code is converted at run-time by the .NET framework into assembly.
This process makes use of something called the JIT compiler to covert the IL code into assembly at run-time. The benefit of this technique is that you can get your end code to run on lots of different hardware. It also means the code can be optimized for that specific processor. This is better than outputting generic code that has to make compromises as it tries to support everything
With AOT compilation, there is no intermediate language code created. Instead, the correct assembly code is created at build/compile time. AOT offers some performance benefits but with a few trade-offs. I won't cover AOT in detail here, however, when you look at Interceptors using the AOT lens, you can see how Microsoft might make use of this feature as they continue to provide support for AOT within .NET.
If you do want to learn more, you can read more about it here.
Enabling Interceptors in .NET 8
The first thing to point out is that if you want to use an interceptor, you need to explicitly enable it as the feature is turned off by default. To be honest, configuring an interceptor using the supplied Microsoft sample code is a complete faff and the end code is very brittle. As you will see this code will probably make you feel a little dirty inside.. be warned!
Online some folks are saying normal .NET devs could potentially make use of interceptors for unit testing obscure or hard-to-test methods, however, as you will see configuring the interceptor is probably harder, than creating a wrapper and testing the problem code directly. This is why this feature is very much targeted towards source code generators only!
In order to intercept a method, you will need to write some code and decorate it with an attribute. Currently, the attribute that you need to use is not included within .NET 8 itself. So your first task will be to manually create an attribute yourself. The code to do this looks like this:
With the attribute created, you can use it to override a method like this:
As you can see from the code you will need to supply three parameters:
- filePath: This is the file path on your local machine. The value will need to point to the file that contains the method you want to replace on your local machine 😞
- line: The exact line number within the file specified above where the method call exists.
- character: The starting line number of the method call.
I personally hate this code syntax for many reasons, however, I kind of get it. If you want to use an interceptor to update code that was created before interceptors were released AND you do not control that code yourself, what options do you have? There is no easy way to tell the compiler the location you want to substitute when you do not have control of that code!
The obvious issue here is that if you refactor or change your existing code in any way, your interceptor will break. The one saving grace is that if you have a misconfiguration you will encounter a compiler error that will tell you that something is wrong. In most instances, the warnings given by .NET are pretty good and will tell you exactly what you have done wrong.
Other note-worthy considerations when implementing an interceptor are:
- Anything that you intercept needs to have the same accessibility modifiers applied to it, so check your
- You can intercept multiple methods with one bit of code.
- You can not create two different interceptors that point to the same location otherwise you will get an
The indicated call is intercepted several timeserror,
Obviously, the code snippet above is useful to explain how the feature works, however, you would not use it in production. The good news is that if you are writing a source generator, there are ways to dynamically generate these values when your code is running. You can see a simple example of configuring a file path in the specification documentation here
In conclusion, if you are a package developer or you need to update code that lives behind closed doors, and whose location will never change, interceptors are something to read up on.
For normal use, unless you are dynamically generating these values, you will be better off using dependency injection rather than making use of this feature.
Happy Coding 🤘