C# 12 中的 Interceptor 特性
Intro
在 C# 12 中引入了一个新的特性,可以借助这个新特性来实现一个简单的 AOP 的逻辑,.NET 8 中的 asp.net core RequestDelegate 和 configuration source generator 也在使用这一特性,我们也来了解一下这个特性和基本的使用吧
Get Started
首先来看一个简单的示例
namespace CSharp12Sample
{
public static class InterceptorSample
{
public static void MainTest()
{
var c = new C();
c.InterceptableMethod();
}
}
public class C
{
public void InterceptableMethod()
{
Console.WriteLine("interceptable");
}
}
}
这里是一个很简单的示例,这个示例里定义了一个 C
类型,其中定义了一个 InterceptableMethod
方法,我们接下来就是要拦截这个方法了
首先我们需要在项目文件中添加一个配置以支持 Interceptor,在 RC2 之前的版本中需要添加 <Features>InterceptorsPreview</Features>
才能使用,RC2 版本中不再需要配置这个了,替而代之的是 <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CSharp12Sample.Generated</InterceptorsPreviewNamespaces>
配置自己 interceptor 逻辑的命名空间,这里我们使用的 CSharp12Sample.Generated
, 自己测试的话需要改成相应的 namespace
我们需要手动声明 InterceptsLocationAttribute
,代码如下:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute
{
}
}
Interceptor 代码如下:
namespace CSharp12Sample.Generated
{
public static class D
{ [System.Runtime.CompilerServices.InterceptsLocation(@"C:\projects\sources\SamplesInPractice\CSharp12Sample\InterceptorSample.cs", line: 10/*L1*/, character: 15/*C1*/)] // refers to the call at (L1, C1)
public static void LoggingInterceptorMethod(this C c)
{
Console.WriteLine("Before...");
c.InterceptableMethod();
Console.WriteLine("After...");
}
}
}
在 interceptor 方法上添加 InterceptsLocation
attribute, 参数是要 intercept 的方法的位置,第一个参数是文件路径,第二个参数是代码所在行数,第三个参数是第几个字符
我们可以借助编辑器或 IDE 来获取调用所在的行和列
运行结果如下:
可以看到我们的方法在执行我们 interceptor 的逻辑,调用前后打印了 Before
和 after
,我们示例里调用了原来的实现,但是我们也可以不调用,这取决于我们自己想要替代的逻辑
Interceptor Generator Sample
这样使用可维护太差,可能不小心加了一个空行,interceptor 就跑不起来了
我们可以结合 source generator 来动态生成 interceptor,下面我们来看个通过 source generator 生成 interceptor 的示例吧
我们类似于前面的示例,定义一个 LoggingGenerator
来生成我们的 interceptor
[Generator(LanguageNames.CSharp)]
public sealed class LoggingGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var methodCalls = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is
InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
Name:
{
Identifier:
{
ValueText: "InterceptableMethod"
}
}
}
}
,
transform: static (context, token) =>
{
var operation = context.SemanticModel.GetOperation(context.Node, token);
if (operation is IInvocationOperation targetOperation
)
{
return new InterceptInvocation(targetOperation);
}
return null;
})
.Where(static invocation => invocation != null);
var interceptors = methodCalls.Collect()
.Select((invocations, _) =>
{
var stringBuilder = new StringBuilder();
foreach (var invocation in invocations)
{
var definition = $$"""
[System.Runtime.CompilerServices.InterceptsLocationAttribute(@"{{invocation.Location.FilePath}}", {{invocation.Location.Line}}, {{invocation.Location.Column}})]
public static void LoggingInterceptorMethod(this CSharp12Sample.C c)
{
System.Console.WriteLine("logging before...");
c.InterceptableMethod();
System.Console.WriteLine("logging after...");
}
""";
stringBuilder.Append(definition);
stringBuilder.AppendLine();
}
return stringBuilder.ToString();
});
context.RegisterSourceOutput(interceptors, (ctx, sources) =>
{
var code = $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute { }
}
namespace CSharp12Sample.Generated
{
public static partial class GeneratedLogging
{
{{sources}}
}
}
""";
ctx.AddSource("GeneratedLoggingInterceptor.g.cs", code);
});
}
}
file sealed class InterceptInvocation(IInvocationOperation invocationOperation)
{
public (string FilePath, int Line, int Column) Location { get; } = GetLocation(invocationOperation);
private static (string filePath, int line, int column) GetLocation(IInvocationOperation operation)
{
// The invocation expression consists of two properties:
// - Expression: which is a `MemberAccessExpressionSyntax` that represents the method being invoked.
// - ArgumentList: the list of arguments being invoked.
// Here, we resolve the `MemberAccessExpressionSyntax` to get the location of the method being invoked.
var memberAccessorExpression = ((MemberAccessExpressionSyntax)((InvocationExpressionSyntax)operation.Syntax).Expression);
// The `MemberAccessExpressionSyntax` in turn includes three properties:
// - Expression: the expression that is being accessed.
// - OperatorToken: the operator token, typically the dot separate.
// - Name: the name of the member being accessed, typically `MapGet` or `MapPost`, etc.
// Here, we resolve the `Name` to extract the location of the method being invoked.
var invocationNameSpan = memberAccessorExpression.Name.Span;
// Resolve LineSpan associated with the name span so we can resolve the line and character number.
var lineSpan = operation.Syntax.SyntaxTree.GetLineSpan(invocationNameSpan);
// Resolve the filepath of the invocation while accounting for source mapped paths.
var filePath = operation.Syntax.SyntaxTree.GetInterceptorFilePath(operation.SemanticModel?.Compilation.Options.SourceReferenceResolver);
// LineSpan.LinePosition is 0-indexed, but we want to display 1-indexed line and character numbers in the interceptor attribute.
return (filePath, lineSpan.StartLinePosition.Line + 1, lineSpan.StartLinePosition.Character + 1);
}
}
完整实例可以参考代码:
https://github.com/WeihanLi/SamplesInPractice/blob/82795842fbecc30ebeae51ca6a74eb82765ad9f7/CSharp12Sample/LoggingGenerator.cs
这个 generator 主要的逻辑是找到 InterceptableMethod()
方法调用的位置,这里示例做了一些简化,没有按类型进行过滤,只过滤了方法,实际使用需要主要一下,最终生成的代码如下:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute { }
}
namespace CSharp12Sample.Generated
{
public static partial class GeneratedLogging
{
[System.Runtime.CompilerServices.InterceptsLocationAttribute(@"C:\projects\sources\SamplesInPractice\CSharp12Sample\InterceptorSample.cs", 10, 15)]
public static void LoggingInterceptorMethod(this CSharp12Sample.C c)
{
System.Console.WriteLine("logging before...");
c.InterceptableMethod();
System.Console.WriteLine("logging after...");
}
}
}
InterceptsLocationAttribute
使用 file
关键词来声明来避免与其他的类库框架发生冲突,interceptor 的代码和前面示例的方法基本一致,只是我们不需要再手动指定调用位置了,运行一下代码试试
More
注意 InterceptsLocationAttribute
的话会发现,可以同时 interceptor 多个调用的,只是我们示例比较简单只声明了一个,但并不限于一个
目前 interceptor 的工作方式类似于 source generator, 所以只能 intercept 正在编译的代码,已经,只支持方法,还不能够完全作为一个 AOP 框架,不过我们确实已经可以利用它来改善一些实现,interceptor 的实现也考虑的 AOT,不需要通过反射来调用
目前 dotnet 支持的调用者信息里我们支持了 CallerFilePath
和 CallerLineNumber
,但是还不支持获取列位置或者字符位置,现在已经有 issue 请求添加 CallerCharacterNumber
attribute 来支持获取字符位置,详细可以参考 issue: https://github.com/dotnet/csharplang/issues/3992
References
https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md
https://github.com/dotnet/csharplang/issues/7009
https://github.com/dotnet/csharplang/issues/3992
https://andrewlock.net/exploring-the-dotnet-8-preview-changing-method-calls-with-interceptors
https://github.com/dotnet/aspnetcore/issues/48289
https://github.com/dotnet/aspnetcore/pull/48817
https://github.com/WeihanLi/SamplesInPractice/blob/82795842fbecc30ebeae51ca6a74eb82765ad9f7/CSharp12Sample/LoggingGenerator.cs
https://github.com/WeihanLi/SamplesInPractice/blob/115ab3c294c2c6227aa5ec479f0e2100e73699c2/CSharp12Sample/InterceptorSample.cs