首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

c#12 实验特性Interceptor如何使用的一个简单但完整的示例

编程知识
2024年08月06日 21:23

一直有很多转载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 逐行调试也可看到断点能进入我们 生成的代码文件中

From:https://www.cnblogs.com/fs7744/p/18346094
本文地址: http://shuzixingkong.net/article/860
0评论
提交 加载更多评论
其他文章 ffmpeg和ffplay常用指令
FFmpeg 常见用法 1. 基本命令结构 ffmpeg [global_options] -i input_file [input_options] output_file [output_options] 2. 将其它格式图片转换为 YUV420p ffmpeg -i input.jpg -pi
数据结构学习之树结构
前段时间刚好在学习机器学习中的决策树,想起多年前学习树这个数据结构的场景,刚好借此机会回归一下知识点。 树是一种非常常见的数据结构,它由节点(Node)和边(Edge)构成。它有如下的一些特征: 1. 根结点(Root Node):树有且只有一个根结点,它是树的顶端结点。 2. 结点(Node):每
爬虫简易说明
想必大家都了解爬虫,也就是爬取网页你所需要的信息 相比于网页繁多的爬虫教程,本篇主要将爬虫分为四个部分,以便你清楚,代码的功能以及使用,这四部分分别为 1.获取到源代码 2.根据网页中的标签特征,获取源代码你所需要的部分 3.想一下如何根据页面的逻辑将一系列的网页自动化抓取 4.保存数据在xlsx等
洛谷P5250 【深基17.例5】木材仓库
【深基17.例5】木材仓库 题目描述 博艾市有一个木材仓库,里面可以存储各种长度的木材,但是保证没有两个木材的长度是相同的。作为仓库负责人,你有时候会进货,有时候会出货,因此需要维护这个库存。有不超过 100000 条的操作: 进货,格式1 Length:在仓库中放入一根长度为 Length(不超过
IntersectionObserver + scrollIntoView 实现电梯导航
电梯导航也被称为锚点导航,当点击锚点元素时,页面内相应标记的元素滚动到视口。而且页面内元素滚动时相应锚点也会高亮。电梯导航一般把锚点放在左右两侧,类似电梯一样。常见的电梯导航效果如下,比如一些官方文档中: 之前可能会用 getBoundingClientRect() 判断元素是否在视口中来实现类似效
IntersectionObserver + scrollIntoView 实现电梯导航 IntersectionObserver + scrollIntoView 实现电梯导航 IntersectionObserver + scrollIntoView 实现电梯导航
增强用户体验:2个功能强大的.NET控制台应用帮助库
前言 对于.NET开发者而言,构建控制台应用程序时,如何提升用户交互的流畅性和满意度,是一个持续探索与优化的话题。今天大姚给大家分享2个功能强大的.NET控制台应用帮助库,希望可以帮助大家能够快速的构建漂亮、强交互性、丰富功能的控制台应用程序。 Terminal.Gui Terminal.Gui是一
增强用户体验:2个功能强大的.NET控制台应用帮助库 增强用户体验:2个功能强大的.NET控制台应用帮助库 增强用户体验:2个功能强大的.NET控制台应用帮助库
给我5分钟,保证教会你在vue3中动态加载远程组件
前言 在一些特殊的场景中(比如低代码、减少小程序包体积、类似于APP的热更新),我们需要从服务端动态加载.vue文件,然后将动态加载的远程vue组件渲染到我们的项目中。今天这篇文章我将带你学会,在vue3中如何去动态加载远程组件。 欧阳写了一本开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。
给我5分钟,保证教会你在vue3中动态加载远程组件 给我5分钟,保证教会你在vue3中动态加载远程组件 给我5分钟,保证教会你在vue3中动态加载远程组件
Redis系列:使用Stream实现消息队列 (图文总结+Go案例)
★ Redis24篇集合 1 先导 我们在《Redis系列14:使用List实现消息队列》这一篇中详细讨论了如何使用List实现消息队列,但同时也看到很多局限性,比如: 不支持消息确认机制,没有很好的ACK应答 不支持消息回溯,无法排查问题和做消息分析 List按照FIFO机制执行,所以存在消息堆积
Redis系列:使用Stream实现消息队列 (图文总结+Go案例)