.NET Tools How-To's

Interceptors – Using C# 12 in Rider and ReSharper

ReSharper and Rider support for C# 12

Welcome to our series, where we take a closer look at the C# 12 language features and how ReSharper and Rider make it easy for you to adopt them in your codebase. If you haven’t yet, download the latest .NET 8 SDK and update your project files!

In this series, we are looking at:

In this part, we will take a closer look at interceptors. Interceptors are an experimental feature available in preview mode in C# 12. It’s important to note that they may be subject to change or even removal in future releases. Though, as you might conclude from the title, ReSharper and Rider already have support for interceptors in their current state!

Background & Syntax

Interceptors (specification) are intended for advanced source-generator scenarios, particularly in AOT compilation. As of now, they are being used in ASP.NET Core minimal APIs to replace reflection-based mapping of HTTP requests with more tailored and efficient implementations.

Have you been busy with source generators already? Then you’ve probably heard about the “additive-only” rule, which means that they could only add new types or extend partial types. This restriction is now partially lifted (all puns intended) through the introduction of interceptors, which allow specific method calls to be redirected to substitutes without any requirements in user code. This technique allows authors of source generators to seamlessly eliminate inefficient code paths (e.g., reflection) and replace them with handcrafted specialized implementations at compile-time.

Before using interceptors in a project, you need to set a couple of properties:

<PropertyGroup>
    <!-- ... -->
    <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);$(RootNamespace).Generated</InterceptorsPreviewNamespaces>
    <!-- <Features>InterceptorsPreview</Features> -->
</PropertyGroup>

The InterceptorsPreviewNamespaces property defines what namespaces are allowed to contain interceptors, which provides a certain level of awareness. The Features property was previously used in .NET 8 previews to enable the interceptors feature but is no longer required.

Now, let’s have a look at a simple example:

// User code
Console.WriteLine("original"); // 🥱
Console.WriteLine("original"); // 🤯

// Generated code
namespace CSharp12
{
    public static class Interceptor
    {
        [System.Runtime.CompilerServices.InterceptsLocation(
            // TODO: Update absolute file path
            filePath: "/path/to/file",
						// TODO: Point to 'Console.{HERE}WriteLine(...)'
            line: 3,
            column: 9)]
        public static void InterceptWriteLine(string? message)
        {
            Console.WriteLine($"INTERCEPTED! Original message was '{message}'");
        }
    }
}

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute(string filePath, int line, int column) : Attribute;
}

Obviously, the two methods are required to match each others signatures. Interceptors rely on the exact location of an interception, both the absolute physical file path as well as the line and column position within the file. If the above example does not work for you, it’s most certainly because the filePath, line, or column arguments have to be updated.

It’s worth noting, that the InterceptsLocationAttribute is (currently) not distributed through any BCL assembly. Therefore, source generators are required to define the type themselves. Since multiple source generators could take advantage of the attribute, it should be considered to define the type file-scoped to avoid duplicates.

So, theory class is over – let’s see some ReSharper and Rider!

Visual Cues & Navigation

A central problem of interceptors is that they can give a false impression of what code is actually executed. Just by looking at our source code, we cannot be sure whether a call is being intercepted or not. Even our good-old goto-declaration navigation can lead us in the wrong direction. And once the behavior of the original and intercepting method diverge, the danger will manifest, and we face a real heisenbug!

Both ReSharper and Rider introduce gutter icons and inlay hints as visual cues for intercepted calls:

Intercepted call with gutter mark and inlay hint icon
Intercepted call with gutter mark and inlay hint icon

You can either click the gutter icon or control-click the inlay hint to navigate to the intercepting method. On the intercepting side, you will see the same visual cues, so you can jump back and forth between the two:

Navigation between intercepted and intercepting method
Navigation between intercepted and intercepting method

In order to show you all about the configuration of interceptor hints, we will need to leave our handcrafted example behind and use a proper source generator included in the dotnet new webapiaot template.

Inlay Hints Configuration

Interceptor hints can be configured from the Alt-Enter menu or by right-clicking the inlay hint itself. From here, you can choose one of the visibility modes (including push-to-hint) or disable the hints completely:

Configuration of visibility for inlay hints
Configuration of visibility for inlay hints

Since the inlay hint icon is reasonably small, we’ve chosen always as the default visibility. You can change the defaults and modify the list of configured source generators from the settings under Editor | Inlay Hints | C# | Interceptor Hints:

Options page for interceptor hints
Options page for interceptor hints

Conclusion

In this post, we’ve discovered interceptors as a promising preview feature and extension to source generators. Try ReSharper 2023.3 or Rider 2023.3 now and never miss to see an interception! If you see any opportunities for additional support, please let us know in the comments below! As always, thank you for reading.

Image credits: Tim Mossholder

image description