本文告诉大家如何在使用 IIncrementalGenerator 进行增量的 Source Generator 生成代码时,如何获取到当前正在分析的程序集所引用的所有的程序集,以及引用的程序集里面的所有类型

这项技术可以用在生成导出类型相关的需求上,比如我想导出我当前程序集里面所有引用的程序集的继承于 IFoo 接口的所有类型,即可采用本文介绍的方法

核心逻辑是在 Compilation 里面拿到 SourceModule 属性,通过 IModuleSymbol 类型的 SourceModule 属性进一步拿到 ImmutableArray<IAssemblySymbol> 类型的 ReferencedAssemblySymbols 属性

这里的 ReferencedAssemblySymbols 属性就是当前的程序集所引用的程序集了

在这些程序集上枚举所有程序集内的语义类型即可获取到所有的类型

以下是详细的例子

为了方便描述本文的技术实现,需要创建三个项目,分别是 App 和 Lib 和 Analyzers 三个项目。在本文末尾将可以找到所有代码的下载方法

这里 App 项目是用来被分析器项目 Analyzers 项目进行分析的。而 Lib 项目则是一个基础库,被 App 项目所引用

在这个例子里面,咱的任务就是在 Analyzers 分析器项目里面编写代码,分析去 App 里面所引用的 Lib 项目里面包含的所有类型

具体的初始化方法就是新建三个 .NET 7 控制台项目,分别命名为 App 和 Lib 和 Analyzers 项目。接着编辑 Lib 项目修改为类库项目,修改的方法就是编辑 csproj 项目文件,替换为如下代码

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> </Project>

接着修改 App 项目的 csproj 项目文件,让 App 项目引用 Lib 项目以及 Analyzers 分析器项目。只有让 App 项目引用 Analyzers 分析器项目,才可以让 Analyzers 分析器项目对 App 项目进行分析,编辑之后的 csproj 项目文件代码如下

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> <ItemGroup>
<ProjectReference Include="..\Lib\Lib.csproj" />
<ProjectReference Include="..\Analyzers\Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup> </Project>

如以上代码,需要让 App 项目引用 Analyzers 分析器项目,设置引用 OutputItemType 为 Analyzer 才可以让 Analyzers 项目被当成 App 项目的分析器

由于 App 项目不需要用到任何在 Analyzers 分析器项目定义的类型,于是也设置了 ReferenceOutputAssembly 为 false 值。通过 OutputItemType="Analyzer" ReferenceOutputAssembly="false" 两个属性即可让 Analyzers 项目只作为 App 项目的分析器存在,不影响 App 项目的其他逻辑,也不会让 App 项目真正引用到 Analyzers 项目里面的任何公开类型

同时设置了 App 项目引用 Analyzers 分析器项目,即可在构建的时候,先构建 Analyzers 分析器项目,再构建 App 项目,确定了项目的构建顺序。于是在 Analyzers 分析器项目里面编写的 IIncrementalGenerator 增量 Source Generator 生成代码逻辑将可以被正常执行

最后来到最重要的 Analyzers 分析器项目。为了能够让 VisualStudio 开森以及让 dotnet 开心,推荐使用的是 netstandard2.0 框架。然后引用上必要的 NuGet 包,修改之后的 csproj 项目文件代码如下

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<LangVersion>latest</LangVersion> <Nullable>enable</Nullable>
</PropertyGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
</ItemGroup> </Project>

详细关于以上 csproj 项目文件代码里的 EnforceExtendedAnalyzerRules 的属性,请参阅 Roslyn 分析器 EnforceExtendedAnalyzerRules 属性的作用

以上的 LangVersion 属性设置为 latest 表示使用最新的语言版本,详细请参阅 VisualStudio 使用三个方法启动最新 C# 功能

通过以上配置即可完成项目的初始化逻辑。回到咱这个例子的任务上,就是在 Analyzers 分析器项目编写代码,分析 App 项目所引用的 Lib 项目里面的存在哪些类型

为了能够让 Analyzers 分析器项目有活干,咱就来给 Lib 项目多添加一些随意的类型

namespace Lib;

public class Base
{
} public class Foo1 : Base
{
} public class Foo2 : IFoo
{
} public class Foo3 : Base, IFoo
{
} public interface IFoo
{
}

完成准备工作之后,接下来开始本文的核心逻辑编写。先在 Analyzers 分析器项目上新建一个继承 IIncrementalGenerator 接口的 FooTelescopeIncrementalGenerator 类型,接下来的核心逻辑将在 FooTelescopeIncrementalGenerator 的 Initialize 开始编写

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text; using Microsoft.CodeAnalysis; namespace Analyzers; [Generator(LanguageNames.CSharp)]
public class FooTelescopeIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
... // 忽略代码
}
}

根据上文的描述,咱需要先从 context 里面的 CompilationProvider 获取到引用的程序集,代码如下

[Generator(LanguageNames.CSharp)]
public class FooTelescopeIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeNameIncrementalValueProvider = context.CompilationProvider.Select((compilation, token) =>
{
... // 忽略代码
});
}
}

通过 compilation 的 SourceModule 属性的 ReferencedAssemblySymbols 即可获取到所有的引用程序集,如以下代码

[Generator(LanguageNames.CSharp)]
public class FooTelescopeIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeNameIncrementalValueProvider = context.CompilationProvider.Select((compilation, token) =>
{
// 获取到所有引用程序集
var referencedAssemblySymbols = compilation.SourceModule.ReferencedAssemblySymbols; ... // 忽略代码
});
}
}

遍历所有引用的程序集。这里为了博客方便,就只获取 Lib 程序集里面的类型

    public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeNameIncrementalValueProvider = context.CompilationProvider.Select((compilation, token) =>
{
// 获取到所有引用程序集
var referencedAssemblySymbols = compilation.SourceModule.ReferencedAssemblySymbols; // 为了方便代码理解,这里只取名为 Lib 程序集的内容…
foreach (var referencedAssemblySymbol in referencedAssemblySymbols)
{
var name = referencedAssemblySymbol.Name; if (name.Contains("Lib"))
{
... // 在这里编写获取程序集所有类型的代码
}
else
{
// 其他的引用程序集,在这里就忽略
}
}
... // 忽略代码
});
}

通过 IAssemblySymbol 的 GlobalNamespace 属性即可获取到顶层的 INamespaceSymbol 符号,通过语义知识可以了解到,类型都是存放在命名空间里面的,只需要对命名空间进行递归即可获取到所有的类型

如以下代码即可递归获取某个 INamespaceSymbol 下的所有类型

    private static IEnumerable<INamedTypeSymbol> GetAllTypeSymbol(INamespaceSymbol namespaceSymbol)
{
var typeMemberList = namespaceSymbol.GetTypeMembers(); foreach (var typeSymbol in typeMemberList)
{
yield return typeSymbol;
} foreach (var namespaceMember in namespaceSymbol.GetNamespaceMembers())
{
foreach (var typeSymbol in GetAllTypeSymbol(namespaceMember))
{
yield return typeSymbol;
}
}
}

给 GetAllTypeSymbol 传入程序集 GlobalNamespace 即可获取到此程序集里面的所有类型,代码如下

    public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeNameIncrementalValueProvider = context.CompilationProvider.Select((compilation, token) =>
{
// 获取到所有引用程序集
var referencedAssemblySymbols = compilation.SourceModule.ReferencedAssemblySymbols; // 为了方便代码理解,这里只取名为 Lib 程序集的内容…
foreach (var referencedAssemblySymbol in referencedAssemblySymbols)
{
var name = referencedAssemblySymbol.Name; if (name.Contains("Lib"))
{
// 获取所有的类型
// 这里 ToList 只是为了方便调试
var allTypeSymbol = GetAllTypeSymbol(referencedAssemblySymbol.GlobalNamespace).ToList();
}
else
{
// 其他的引用程序集,在这里就忽略
}
}
... // 忽略代码
});
}

以上拿到的 allTypeSymbol 就是引用的 Lib 程序集里面的所有类型。为了测试咱的分析器代码是否正确,可以尝试将收集到的 Lib 程序集里面的所有类型的记录输出作为一个源代码生成

{% raw %}

    public void Initialize(IncrementalGeneratorInitializationContext context)
{
Debugger.Launch(); var typeNameIncrementalValueProvider = context.CompilationProvider.Select((compilation, token) =>
{
var typeNameList = new List<string>(); // 获取到所有引用程序集
var referencedAssemblySymbols = compilation.SourceModule.ReferencedAssemblySymbols; // 为了方便代码理解,这里只取名为 Lib 程序集的内容…
foreach (IAssemblySymbol? referencedAssemblySymbol in referencedAssemblySymbols)
{
var name = referencedAssemblySymbol.Name; if (name.Contains("Lib"))
{
// 获取所有的类型
// 这里 ToList 只是为了方便调试
var allTypeSymbol = GetAllTypeSymbol(referencedAssemblySymbol.GlobalNamespace).ToList(); foreach (var typeSymbol in allTypeSymbol)
{
typeNameList.Add(typeSymbol.ToDisplayString());
}
}
else
{
// 其他的引用程序集,在这里就忽略
}
} return typeNameList;
}); context.RegisterSourceOutput(typeNameIncrementalValueProvider, (productionContext, list) =>
{
var code = $@"
public static class FooHelper
{{
public static IEnumerable<string> GetAllTypeName()
{{
{(string.Join("\r\n", list.Select(t => $@"yield return ""{t}"";")))}
}}
}}";
productionContext.AddSource("FooHelper", code);
});
}

{% endraw %}

如以上代码就在代码生成器里面生成了名为 FooHelper 的类型,这个类型将会返回 Lib 程序集里面的所有的类型

接下来编辑 App 项目的 Program.cs 文件,替换为如下代码

foreach (var name in FooHelper.GetAllTypeName())
{
Console.WriteLine(name);
}

假设分析器项目代码编写正确,那就可以成功输出 Lib 程序集里面的所有类型到控制台

试试运行一下项目,看看写的对不对吧

本文所有代码放在 githubgitee 上,可以通过以下方式获取整个项目的代码

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin d0b5dc0af9c9f4ff3c18a2212200b492e3edbc08

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin d0b5dc0af9c9f4ff3c18a2212200b492e3edbc08

获取代码之后,进入 LurwajedalwemcejemQallneleecairwhair 文件夹

IIncrementalGenerator 获取引用程序集的所有类型的更多相关文章

  1. 使用typeid(变量或类型).name()来获取常量或变量的类型---gyy整理

    使用typeid(变量或类型).name()来获取常量或变量的类型 <typeinfo>  该头文件包含运行时类型识别(在执行时确定数据类型)的类 typeid的使用   typeid操作 ...

  2. Ⅴ.spring的点点滴滴--引用其他对象或类型的成员

    承接上文 引用其他对象或类型的成员 .net篇(环境为vs2012+Spring.Core.dll v1.31) public class Person { public string Name { ...

  3. C# -- 使用反射(Reflect)获取dll文件中的类型并调用方法

    使用反射(Reflect)获取dll文件中的类型并调用方法 需引用:System.Reflection; 1. 使用反射(Reflect)获取dll文件中的类型并调用方法(入门案例) static v ...

  4. XML序列化 判断是否是手机 字符操作普通帮助类 验证数据帮助类 IO帮助类 c# Lambda操作类封装 C# -- 使用反射(Reflect)获取dll文件中的类型并调用方法 C# -- 文件的压缩与解压(GZipStream)

    XML序列化   #region 序列化 /// <summary> /// XML序列化 /// </summary> /// <param name="ob ...

  5. C#开发BIMFACE系列9 服务端API之获取应用支持的文件类型

    系列目录     [已更新最新开发文章,点击查看详细] BIMFACE最核心能力之一是工程文件格式转换.无需安装插件,支持数十种工程文件格式在云端转换,完整保留原始文件信息.开发者将告别原始文件解析烦 ...

  6. Java web项目引用java项目,类型找不到

    Java web项目引用java项目,类型找不到 错误信息: java.lang.ClassNotFoundException: org.codehaus.jackson.map.ObjectMapp ...

  7. java利用反射获取类的属性及类型

    java利用反射获取类的属性及类型. import java.lang.reflect.Field; import java.math.BigDecimal; import java.util.Map ...

  8. 获取jdk支持的编码类型

    //获取jdk支持的编码类型 Map<String,Charset> maps = Charset.availableCharsets(); for(Map.Entry<String ...

  9. VS2017 加载项目 :未找到框架“.NETFramework,Version=v4.7”的引用程序集(出坑指南)

    报出的错误为: 错误MSB3644: 未找到框架“.NETFramework,Version=v4.7”的引用程序集.若要解决此问题,请安装此框架版本的 SDK 或 Targeting Pack,或将 ...

  10. Perl 引用:引用就是指针,Perl 引用是一个标量类型可以指向变量、数组、哈希表(也叫关联数组)甚至子程序。

    Perl 引用引用就是指针,Perl 引用是一个标量类型可以指向变量.数组.哈希表(也叫关联数组)甚至子程序,可以应用在程序的任何地方. 1.创建引用1.使用斜线\定义变量的时候,在变量名前面加个\, ...

随机推荐

  1. 记录--纯CSS实现骚气红丝带

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 在本文中,我们将探讨如何使用 CSS 以最少的代码创造出精美的 CSS 丝带形状,并最终实现下面这个效果: 下面我们使用html和css来 ...

  2. #动态规划#CF889E Mod Mod Mod

    题目传送门 分析 这道题有一个很妙的地方就是将一段前缀整体一起做. 设 \(dp[i][j]\) 表示\(x\) 被前 \(i\) 个数取模后答案最大,并且 \(j\) 为取得此答案的最大值 最后再对 ...

  3. #2-SAT,平面图#洛谷 3209 [HNOI2010] 平面图判定

    题目传送门 分析 首先一张图是平面图的必要条件为 \(m\leq 3*n-6\), 然后考虑到这题的图存在哈密尔顿回路,也就是说非环边因为跨立形成奇环即为无解 那么直接拆点跑2-SAT就可以了 代码 ...

  4. #扫描线,线段树#nssl 1459 空间复杂度

    分析 由于\(k\leq 10\)所以考虑用总方案减去经过两个差的绝对值\(\leq k\)的点的路径数 分类讨论一下发现要处理祖先关系和其它关系两种情况,考虑怎么去重,可以将这些答案看作一个个矩形, ...

  5. 使用8086汇编驱动SHT11传感器

    前言:使用Proteus 7.8仿真软件实现8086接入SHT11温湿度传感器(实现读取温度数据部分功能),并学习如何在没有集成硬件控制下串行总线的驱动方式,汇编的精髓就是寄存器的操作.各种寻址方式. ...

  6. 基于vue3的Crontab组件

    网上找的没有满意的,决定从若依前后端分离其前端vue2中的crontab进行转换,先上效果 若依: 改后: v2转v3没什么难度,其中有大量的将 this.*** 替换为 ***.value,笔者写了 ...

  7. 【直播回顾】OpenHarmony知识赋能第八期:手把手教你实现涂鸦小游戏

     OpenHarmony第八期知识赋能直播已经在9月29日圆满落幕!从9月15日起,资深OS框架开发工程师巴延兴老师于每周四进行分享,通过实现涂鸦小游戏来帮助大家全面了解ArkUI框架的应用,拓宽知识 ...

  8. Java 包和 API 深度解析:组织代码,避免命名冲突

    Java 包和 API Java 中的包 用于将相关的类分组在一起.可以将其视为文件目录中的一个文件夹.我们使用包来避免名称冲突,并编写更易于维护的代码. 包分为两类: 内置包(来自 Java API ...

  9. 一、Unity调用Xcode封装方法(工程引用文件)

    1.Xcode新建Static Library 工程 (我起的名字是UnityExtend 可以在接下来的图中看到) 2.打包unity ios工程 unity打包ios 打出Xcode工程 3.打开 ...

  10. 用存储过程和 JAVA 写报表数据源有什么弊端?

    我们在报表开发中经常会使用存储过程准备数据,存储过程支持分步计算,可以实现非常复杂的计算逻辑,为报表开发带来便利.所以,报表开发中这样的存储过程并不少见: 3008 行,141KB 的存储过程,会给报 ...