# Interceptors working group notes for 20th March 2023

We explored the extension shadowing concept and compared with interceptors to determine where to invest to meet our short and long term goals.

## Extension shadowing

https://github.com/dotnet/csharplang/issues/7061

The working group had several observations to make about this approach:

### Constant propagation

While generalized compile-time constant propagation is intractible, there are ways to scope it down and make the "dispatch blocks" proposed in this approach work well. For example, we can mark specific methods or parameters to indicate that the AOT compiler should do some interprocedural analysis, e.g. `void M([ConstantExpected] string param)`. The AOT compiler already takes hints on whether to do interprocedural analysis based on knowledge of Roslyn compiler-generated symbols, and `[ConstantExpected]` actually already exists.

The suggestion in this case would be not to inline these "dispatch block" methods, but instead to trim as though the arguments coming in are constant, eliminating any dead branches within the block.

Roslyn has fairly sophisticated emit behavior when switching on strings which complicates the AOT compiler's ability to optimize in cases like these. We might want to give some hints to downstream compilers to make it easier for them. It helps that these extensions will usually be used in the same assembly they are declared in.

We also gave some consideration to whether Roslyn could adjust its codegen based on presence of these attributes. For example, Roslyn hasn't really done method inlining to date, but we might introduce it in order to handle scenarios like these.

```cs
// ASP.NET library
implicit extension RouteBuilderExtension for IEndpointRouteBuilder
{
    public void MapGet(
        string route,
        Delegate handler) { }
}

// Generated.cs
implicit extension SpecializedRouteBuilder : RouteBuilderExtension
{
    // produced by the source generator:
    public void MapGet(
        string route,
        Delegate handler,
        [ConstantExpected] [CallerFilePath] string? filePath = null,
        [CallerLineNumber] int lineNumber = -1,
        [CallerCharacterNumber] int characterNumber = -1) // https://github.com/dotnet/csharplang/issues/3992
    {
        switch (filePath, lineNumber, characterNumber)
        {
            case ("User.main.cs", 5, 11): MapProducts(handler);
            case ("User.main.cs", 6, 11): MapUsers(handler);
            // imagine there are many many more cases than these...
            default: throw new Exception("unknown route");
        }
    }
    
    // roughly what the AOT compiler ends up generating:
    public void MapGet(
        string route,
        Delegate handler,
        [ConstantExpected] [CallerFilePath] string? filePath = null,
        [CallerLineNumber] int lineNumber = -1,
        [CallerCharacterNumber] int characterNumber = -1) // https://github.com/dotnet/csharplang/issues/3992
    {
        switch (filePath, lineNumber, characterNumber)
        {
            // The path which adds a handler for 'Products' is unreachable, so it is elided here.
            case ("User.main.cs", 6, 11): MapUsers(handler);
            // imagine there are many many more cases than these...
            default: throw new Exception("unknown route");
        }
    }
}
```

### Denoting shadowing, and shadowing non-extensions

If we went with this approach, we would need extensions to be able to shadow non-extension methods. This feels fairly risky.

It isn't clear whether a "shadowable" modifier on base members is needed or not, or if some new usage of the "new" modifier in extensions is needed or not. It feels like requiring too much opt-in will be burdensome, but not enough opt-in might confuse users.

Additionally, the actual lookup rules could be quite difficult to get right. Extensions will be able to be declared in complex hierarchies, with members varying in signature and potential "betterness". It's possible new overload resolution rules would be needed in order to make things work here, and it may be very difficult to formulate the right rule, build it and test it.

We believe that extension shadowing would be more work across more teams than interceptors would be.

## Interceptors

There are some toolability concerns for InterceptsLocationAttribute.

It may be problematic if locations aren't normalized. Directory separators, etc. differ across platforms. This means that cross-platform projects would basically only work if their interceptors are created exclusively by source generators during the build. Of course, the "brittleness" problem with locations affects that as well.

We're not certain to what extent to invest in IDE support. One particular concern we have is just how many editor features would need to be updated to indicate new information about the meaning of a call. Also, the possibility of interception may make things like incrementally analyzing method edits more expensive. This is what pushed us toward including a syntactic marker like `#Interceptable()` in earlier passes on the feature.

However, in terms of program behavior and perf characteristics, we think this is easier to get right for the scenarios we care about than extension shadowing. The concerns about inlining and trimmability are removed.

## Conclusion

We think that interceptors are the better of the two approaches to address key scenarios for .NET 8.

We will treat interceptors as an *experimental compiler feature* for .NET 8. The feature will have diagnostics for misuse, but it will have a limited amount of design-time traceability. The feature may have breaking changes or be removed in a future release.
