C# 12 中的 Interceptor 特性

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 之前的版本中需要添加 InterceptorsPreview 才能使用,RC2 版本中不再需要配置这个了,替而代之的是 $(InterceptorsPreviewNamespaces);CSharp12Sample.Generated

配置自己 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:projectssourcesSamplesInPracticeCSharp12SampleInterceptorSample.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 来获取调用所在的行和列

运行结果如下:

simple interceptor output

可以看到我们的方法在执行我们 interceptor 的逻辑,调用前后打印了 Beforeafter,我们示例里调用了原来的实现,但是我们也可以不调用,这取决于我们自己想要替代的逻辑

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 ;
})
.Where(static invocation => invocation != );

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 = $$"""
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
//------------------------------------------------------------------------------

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() 方法调用的位置,这里示例做了一些简化,没有按类型进行过滤,只过滤了方法,实际使用需要主要一下,最终生成的代码如下:

//------------------------------------------------------------------------------
//
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
//------------------------------------------------------------------------------

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:projectssourcesSamplesInPracticeCSharp12SampleInterceptorSample.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 的代码和前面示例的方法基本一致,只是我们不需要再手动指定调用位置了,运行一下代码试试

generator interceptor sample

More

注意 InterceptsLocationAttribute 的话会发现,可以同时 interceptor 多个调用的,只是我们示例比较简单只声明了一个,但并不限于一个

目前 interceptor 的工作方式类似于 source generator, 所以只能 intercept 正在编译的代码,已经,只支持方法,还不能够完全作为一个 AOP 框架,不过我们确实已经可以利用它来改善一些实现,interceptor 的实现也考虑的 AOT,不需要通过反射来调用

目前 dotnet 支持的调用者信息里我们支持了 CallerFilePathCallerLineNumber ,但是还不支持获取列位置或者字符位置,现在已经有 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


展开阅读全文

页面更新:2024-02-16

标签:特性   示例   字符   逻辑   定义   声明   位置   参数   代码   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top