C# Interceptors

November 14, 2023

Interceptors are a new feature in C# 12. They’re also an extremely interesting feature - they remind me a lot of Weavers. The idea there was very similar - you could write code that could be integrated into the IL, but interceptors have brought this forward, so that it changes the code that’s generated.

Disclaimer: this feature is in preview. The documentation is definitely not complete and even Microsoft say it may change. If you follow this article and it doesn’t work, that’s probably why.

Set-up

Okay, I wrote this using .Net 8, C# 12 and VS 17.8.0 - Preview 7. I’m not saying you need all of these (well, you kind of need the C# and .Net version as a minimum), but that’s what I used.

You need to add some things to the project file:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
	<OutputType>Exe</OutputType>
	<TargetFramework>net8.0</TargetFramework>
	<ImplicitUsings>enable</ImplicitUsings>
	<Nullable>enable</Nullable>

	<Features>InterceptorsPreview</Features>
	<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Microsoft.AspNetCore.Http.Generated;ConsoleApp17</InterceptorsPreviewNamespaces>
	<LangVersion>preview</LangVersion>
</PropertyGroup>

</Project>

You need to tell it both that you are using this feature (Features) and you need to tell it which namespaces it’s allowed to intercept. This is an opt in feature, and it will not work (or compile) without it.

The Basics

To start off, let’s create a very simply example. Here’s what the project looks like:

Project set-up

There’s three files there - let’s go through them:

InterceptsLocationAttribute.cs

namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class InterceptsLocationAttribute : Attribute
{
    public InterceptsLocationAttribute(string filePath, int line, int character)
    {
    }
}

This isn’t built into the base class library - presumably because it’s an experimental feature.

Program.cs

This is just the code of the program; for example:

var x = new TestClass();
x.MyMethod();

class TestClass
{
    public void MyMethod()
    {
        Console.WriteLine("Test");
    }
}

This is relatively simple: the class has a method that prints something to the screen, that class is instantiated and called. If you run that code, it should print Test.

Okay - onto the final piece…

Interceptor.cs

This is where you can run the interceptor:

static class Interceptor
{
    [InterceptsLocation(
        "C:\\Users\\paul\\source\\repos\\ConsoleApp17\\ConsoleApp17\\Program.cs",
        line: 6, character: 3)]
    public static void Intercept(this TestClass testClass)
    {
        Console.WriteLine("Test2");
    }
}

Let’s start with the basics: this will replace the code at line 6, character 3 with a call to the method Intercept. The call on line 3 must be to the class that you’re intercepting (you tell it that because this is an extension method).

If you now run this code, it will output Test2!

Okay - that’s interesting. You’ll see code like this in the links below (although they all seem to be missing at least one piece of the puzzle). However, let’s play a little more and see what else we can do.

Advanced (or, less basic)

Let’s start with this:

var x = new TestClass();
var dateTime = new DateTime(2023, 11, 14);
x.MyMethod(dateTime);

class TestClass
{
    public void MyMethod(DateTime dateTime)
    {
        Console.WriteLine($"The date is: {dateTime}");
    }
}

We’ve introduced a variable here - which means the interceptor needs to change slightly:

static class Interceptor
{
    [InterceptsLocation(
        "C:\\Users\\paul\\source\\repos\\ConsoleApp17\\ConsoleApp17\\Program.cs",
        line: 3, character: 3)]
    public static void Intercept(this TestClass testClass, DateTime dateTime)
    {
        Console.WriteLine("Test2");
    }
}

Here, all we’ve done is introduce a parameter to the interceptor, so the signatures match.

We can also change the code like this:


var x = new TestClass();
var dateTime = x.MyMethod();
Console.WriteLine(dateTime);

class TestClass
{
    public DateTime MyMethod()
    {
        return DateTime.Now;        
    }
}

And intercept like this:

static class Interceptor
{
    [InterceptsLocation(
        "C:\\Users\\paul\\source\\repos\\ConsoleApp17\\ConsoleApp17\\Program.cs",
        line: 2, character: 18)]
    public static DateTime Intercept(this TestClass testClass)
    {
        //Console.WriteLine("Test2");
        return new DateTime(2023, 11, 10);
    }
}

This will output the provided date, rather than the current:

Date output

This kind of starts making you think of Fakes, however, you can’t replace static methods; for example:

var x = new TestClass();
var dateTime = DateTime.Parse("1/2/2021");
Console.WriteLine(dateTime);

This cannot be intercepted, because it’s a static method.

Summary

What this means is that this feature can’t be used to replace functionality for testing any more than a mock can. It does mean that you can potentially avoid using interfaces solely for the purpose of mocking them out, though.

References

Interceptors - Stack Overflow Question

New Features in C# - MS Blog

Blog - .Net 8 Interceptors

GitHub Example - Interceptors

Roslyn GitHub Docs

Microsoft - What’s New in C#



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024