c#12 实验特性Interceptor如何使用的一个简单但完整的示例
一直有很多转载dotnet对Interceptor说明文档的,但鲜有说明Interceptor如何使用的,这里写一篇简单示例来展示一下
c# 12 实验特性Interceptor 是什么?
官方解释如下(其实简单说就是语言特性中内置的静态编织方式的aop功能,不同于其他il修改代码的方式,使用上得结合source generater 来生成代码 )
拦截器是一种方法,该方法可以在编译时以声明方式将对可拦截方法的调用替换为对其自身的调用。 通过让拦截器声明所拦截调用的源位置,可以进行这种替换。 拦截器可以向编译中(例如在源生成器中)添加新代码,从而提供更改现有代码语义的有限能力。
在源生成器中使用拦截器修改现有编译的代码,而非向其中添加代码。 源生成器将对可拦截方法的调用替换为对拦截器方法的调用。
如果你有兴趣尝试拦截器,可以阅读功能规范来了解详细信息。 如果使用该功能,请确保随时了解此实验功能的功能规范中的任何更改。 最终确定功能后将在微软文档站点上添加更多指导。
示例
示例目的
这里我们用一个简单的 static method 作为 我们改写方法内容的目标
public static partial class DBExtensions
{
public static string TestInterceptor<T>(object o)
{
return o.GetType().ToString();
}
}
这样的静态方法,我们假设改写的目标为 返回 o 参数的其中一个string类型的属性值
所以应该可以通过如下的 UT 方法
[Fact]
public void CallNoError()
{
Assert.Equal("sss", DBExtensions.TestInterceptor<AEnum>(new { A = "sss", C= "ddd" }));
}
如何实现
第一步 建立类库
建立一个 netstandard2.0 的类库并设置如下
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
第二步 设置 UT 项目开启 Interceptor 功能
Generated 目录生成代码文件其实是非必须的,但是为了方便大家看到 source generater 生成的代码文件内容,对于我们初次尝试source generater很有帮助
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Test.AOT</InterceptorsPreviewNamespaces>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SlowestEM.Generator2\SlowestEM.Generator2.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)">
<RemoveDir Directories="Generated" />
</Target>
<ItemGroup>
<Compile Remove="Generated\**" />
<Content Include="Generated\**" />
</ItemGroup>
</Project>
第三步 实现 InterceptorGenerator
[Generator(LanguageNames.CSharp)]
public class InterceptorGenerator : IIncrementalGenerator
{
}
这里的 IIncrementalGenerator 为source generater 更强设计的一代接口,有更强的性能和更方便的能力, 感兴趣可以参考incremental-generators.md
接着我们来实现接口
[Generator(LanguageNames.CSharp)]
public class InterceptorGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var nodes = context.SyntaxProvider.CreateSyntaxProvider(FilterFunc, TransformFunc) // FilterFunc 为遍历语法节点时提供给我们过滤语法节点范围 ,TransformFunc 为我们转换为语法处理数据
.Where(x => x is not null)
.Select((x, _) => x!);
var combined = context.CompilationProvider.Combine(nodes.Collect());
context.RegisterImplementationSourceOutput(combined, Generate); // Generate 是最终实际转换代码文件的方法
}
}
接着我们来实现 FilterFunc
private bool FilterFunc(SyntaxNode node, CancellationToken token) // 这里我们只过滤 调用 TestInterceptor 方法的地方
{
if (node is InvocationExpressionSyntax ie && ie.ChildNodes().FirstOrDefault() is MemberAccessExpressionSyntax ma)
{
return ma.Name.ToString().StartsWith("TestInterceptor");
}
return false;
}
// 可以看出比之前的 ISyntaxContextReceiver 更为简单
接着我们来实现 TransformFunc
private TestData TransformFunc(GeneratorSyntaxContext ctx, CancellationToken token)
{
try
{
// 再次过滤确保只需要处理 方法调用的场景
if (ctx.Node is not InvocationExpressionSyntax ie
|| ctx.SemanticModel.GetOperation(ie) is not IInvocationOperation op)
{
return null;
}
// 由于我们测试使用的是 匿名类初始化 语句,参数是 object,所以生成时实际有隐式转换
var s = op.Arguments.Select(i => i.Value as IConversionOperation).Where(i => i is not null)
.Select(i => i.Operand as IAnonymousObjectCreationOperation) // 查找匿名类的第一个 为 string 的属性
.Where(i => i is not null)
.SelectMany(i => i.Initializers)
.Select(i => i as IAssignmentOperation)
.FirstOrDefault(i => i.Target.Type.ToDisplayString() == "string");
// 生成 返回 第一个 为 string 的属性的 方法
return new TestData { Location = op.GetMemberLocation(), Method = @$"
internal static {op.TargetMethod.ReturnType} {op.TargetMethod.Name}_test({string.Join("", op.TargetMethod.Parameters.Select(i => @$"{i.Type} {i.Name}"))})
{{
{(s == null ? "return null;" : $@"
dynamic c = o;
return c.{(s.Target as IPropertyReferenceOperation).Property.Name};
") }
}}
" };
}
catch (Exception ex)
{
Debug.Fail(ex.Message);
return null;
}
}
// 这里我们随意创建一个类来方便我们处理中间数据
public class TestData
{
public Location Location { get; set; }
public string Method { get; set; }
}
public static class TypeSymbolHelper
{
// 获取 语法节点所在文件物理路径
internal static string GetInterceptorFilePath(this SyntaxTree? tree, Compilation compilation)
{
if (tree is null) return "";
return compilation.Options.SourceReferenceResolver?.NormalizePath(tree.FilePath, baseFilePath: null) ?? tree.FilePath;
}
public static Location GetMemberLocation(this IInvocationOperation call)
=> GetMemberSyntax(call).GetLocation();
// 很不幸,由于拦截器 替换必须代码文件物理文件位置,行号 列号都必须准确, 比如 xxx.TestInterceptor, 比如要 TestInterceptor 的准确位置, 如果从 xxx. 开始都不正确,编译无法通过
// 所以这里有一个比较繁琐的方法来帮助我们准确找到 位置
public static SyntaxNode GetMemberSyntax(this IInvocationOperation call)
{
var syntax = call?.Syntax;
if (syntax is null) return null!; // GIGO
foreach (var outer in syntax.ChildNodesAndTokens())
{
var outerNode = outer.AsNode();
if (outerNode is not null && outerNode is MemberAccessExpressionSyntax)
{
// if there is an identifier, we want the **last** one - think Foo.Bar.Blap(...)
SyntaxNode? identifier = null;
foreach (var inner in outerNode.ChildNodesAndTokens())
{
var innerNode = inner.AsNode();
if (innerNode is not null && innerNode is SimpleNameSyntax)
identifier = innerNode;
}
// we'd prefer an identifier, but we'll allow the entire member-access
return identifier ?? outerNode;
}
}
return syntax;
}
}
接着我们来实现 Generate
private void Generate(SourceProductionContext ctx, (Compilation Left, ImmutableArray<TestData> Right) state)
{
try
{
// 这里主要是生成 InterceptsLocation
var s = string.Join("", state.Right.Select(i =>
{
var loc = i.Location.GetLineSpan();
var start = loc.StartLinePosition;
return @$"[global::System.Runtime.CompilerServices.InterceptsLocationAttribute({SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(i.Location.SourceTree.GetInterceptorFilePath(state.Left)))},{start.Line + 1},{start.Character + 1})]
{i.Method}";
}));
var ss = $@"
namespace Test.AOT
{{
file static class GeneratedInterceptors
{{
{s}
}}
}}
namespace System.Runtime.CompilerServices
{{
// this type is needed by the compiler to implement interceptors - it doesn't need to
// come from the runtime itself, though
[global::System.Diagnostics.Conditional(""DEBUG"")] // not needed post-build, so: evaporate
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
sealed file class InterceptsLocationAttribute : global::System.Attribute
{{
public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber)
{{
_ = path;
_ = lineNumber;
_ = columnNumber;
}}
}}
}}
";
ctx.AddSource((state.Left.AssemblyName ?? "package") + ".generated.cs", ss);
}
catch (Exception ex)
{
Debug.Fail(ex.Message);
}
}
目前需要自定义InterceptsLocationAttribute, 所以需要生成一个,
这样做的目前主要是目前还是实验特性 ,api 设计还在变化,并且其实物理文件位置现在已被认可非常不方便,已设计新的方式,但是相关设计还不太方便使用,所以这里我们也还是使用物理位置的方式
感兴趣的童鞋可以参考interceptors.md
最后一步 编译试试
如果我们编译程序,就会看见生成了这样的文件代码
namespace Test.AOT
{
file static class GeneratedInterceptors
{
[global::System.Runtime.CompilerServices.InterceptsLocationAttribute("D:\\code\\dotnet\\SlowestEM\\test\\UT\\GeneratorUT\\StartMethod.cs",26,35)]
internal static string TestInterceptor_test(object o)
{
dynamic c = o;
return c.A;
}
}
}
namespace System.Runtime.CompilerServices
{
// this type is needed by the compiler to implement interceptors - it doesn't need to
// come from the runtime itself, though
[global::System.Diagnostics.Conditional("DEBUG")] // not needed post-build, so: evaporate
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
sealed file class InterceptsLocationAttribute : global::System.Attribute
{
public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber)
{
_ = path;
_ = lineNumber;
_ = columnNumber;
}
}
}
如果运行ut ,结果也正确, debug 逐行调试也可看到断点能进入我们 生成的代码文件中
c#12 实验特性Interceptor如何使用的一个简单但完整的示例的更多相关文章
- UFT\QTP 12 新特性
UFT\QTP 12 新特性 http://blog.csdn.net/testing_is_believing/article/details/22310297
- Node.js V0.12 新特性之性能优化
v0.12悠长的开发周期(已经过去九个月了,并且还在继续,是有史以来最长的一次)让核心团队和贡献者们有充分的机会对性能做一些优化. 本文会介绍其中最值得注意的几个. http://www.infoq. ...
- atitit.Oracle 9 10 11 12新特性attilax总结
atitit.Oracle 9 10 11 12新特性 1. ORACLE 11G新特性 1 1.1. oracle11G新特性 1 1.2. 审计 1 1.3. 1. 审计简介 1 1.4. ...
- Java SE 12 新增特性
Java SE 12 新增特性 作者:Grey 原文地址:Java SE 12 新增特性 源码 源仓库: Github:java_new_features 镜像仓库: GitCode:java_new ...
- ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则
原文:ASP.NET MVC基于标注特性的Model验证:一个Model,多种验证规则 对于Model验证,理想的设计应该是场景驱动的,而不是Model(类型)驱动的,也就是对于同一个Model对象, ...
- 实验---反汇编一个简单的C程序(杨光)
反汇编一个简单的C程序 攥写人:杨光 学号:20135233 ( *原创作品转载请注明出处*) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163 ...
- 一个简单的ns2实验全过程
实验名称:比较tcp和udp的丢包行为 试验目的:1. 熟练用ns2做网络仿真试验的整个流程:2. 练习写tcl脚本,了解怎么应用http和rtp:3. 练习用awk处理trace数据,了解怎么计算丢 ...
- python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用
python操作三大主流数据库(12)python操作redis的api框架redis-py简单使用 redispy安装安装及简单使用:https://github.com/andymccurdy/r ...
- ASP.NET Core Identity 验证特性 - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core Identity 验证特性 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Identity 验证特性 上一章节我们简单介绍了 ...
- ? 原创: 铲子哥 搜狗测试 今天 shell编程的时候,往往不会把所有功能都写在一个脚本中,这样不太好维护,需要多个脚本文件协同工作。那么问题来了,在一个脚本中怎么调用其他的脚本呢?有三种方式,分别是fork、source和exec。 1. fork 即通过sh 脚本名进行执行脚本的方式。下面通过一个简单的例子来讲解下它的特性。 创建father.sh,内容如下: #!/bin/bas
? 原创: 铲子哥 搜狗测试 今天 shell编程的时候,往往不会把所有功能都写在一个脚本中,这样不太好维护,需要多个脚本文件协同工作.那么问题来了,在一个脚本中怎么调用其他的脚本呢?有三种方式,分别 ...
随机推荐
- thinkpad t490触摸板失灵解决方法
笔记本电脑之前触摸板使用正常,可能在某次更新之后,发现失灵不可用. 解决方法: 更新或滚动触摸板驱动程序 当您在设备管理器中时,右键单击列表中的触摸板(可能称为Dell TouchPad,Lenovo ...
- 简单的css3头像旋转与3D旋转效果
Tips:当你看到这个提示的时候,说明当前的文章是由原emlog博客系统搬迁至此的,文章发布时间已过于久远,编排和内容不一定完整,还请谅解` 简单的css3头像旋转与3D旋转效果 日期:2017-7- ...
- 如何在 VSCode 中配置和编写 LINGO
目录 如何在 VSCode 中配置和编写 LINGO 安装 VSCode 扩展 LINGO 脚本文件与 runlingo 命令 LINGO 命令行交互和脚本文件 配置 Visual Stdio Cod ...
- yolov1-yolov5 网络结构&正负样本筛选&损失计算
学习yolo系列,最重要的,最核心的就是网络模型.正负样本匹配.损失函数等三个方面.本篇汇总了yolov1-yolov5等5个版本的相关知识点,主要看点是在yolo框架搭建.初学者可以通过相关篇章搭建 ...
- 在Ubuntu 18.04 安装 adb
Ubuntu下安装ADB 背景 电脑上的USB口有问题,不方便调试:发现用于开发的服务器就在工位旁边. 先拿过来用一下. Ubuntu:18.04 做法 安装adb 做法有很多种,列举下列2种. 下载 ...
- STM32 CubeMX 学习:有关说明
背景 STM32 是我以前学过的,而很久没有整理过的.因为之前学习的时间比较早,再加上各种资料要么不成熟,要么不齐全:再加上自己一开始没有比较完善的学习经验:以至于我的学习并不扎实. 趁着 STM 的 ...
- Python爬取小说+Servlet+C3P0+MVC构建小说api
一.摘要: 使用python爬取网络小说数据存入数据库,利用C3P0数据库连接池获取数据库数据,采用MVC三层架构对数据库数据进行操作,输出JSON格式数据到前端页面 二.内容: 1.gitee外链消 ...
- 云服务器安装宝塔Linux面板教程(建议收藏)
一.简介 宝塔面板是一款简单好用的服务器运维面板.它支持一键LAMP/LNMP/集群/监控/网站/FTP/数据库/JAVA等100多项服务器管理功能.对于新手用云服务器来建站的话,宝塔面板是一个非 ...
- 解决方案 | win10任务栏假死,桌面鼠标可以动但是无法点击任务栏图标
1 背景 今天电脑不知道什么原因,鼠标出现了无法点击任务栏图标的情况,但是桌面上可以晃动. 2 解决过程 (方法1-3对我无效,但是不代表对你们无效,) 方法1:重启资源管理器. 方法2:电脑重启.或 ...
- 外部网关协议BGP
不能全部使用RIP与OSPF的原因有二:互联网规模太大,自治系统间路由选择困难:自治系统间路由选择必须考虑有关策略. 在每一个自治系统中有两种不同功能的路由器,边界路由器和内部路由器. BGP四种报文 ...