前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用 Source Generator 在编译你的 .NET 项目时自动生成代码

使用 Source Generator 在编译你的 .NET 项目时自动生成代码

作者头像
walterlv
发布2023-10-23 11:25:48
4600
发布2023-10-23 11:25:48
举报

本文将带你为你的某个库添加自动生成代码的逻辑。

本文以 dotnetCampus.Ipc 项目为例,来说明如何为一个现成的 .NET 类库添加自动生成代码的功能。这是一个在本机内进行进程间通信的库,在你拥有一个 IPC 接口和对应的实现之后,本库还会自动帮你生成通过 IPC 代理访问的代码。由于项目加了 Roslyn 的 SourceGenerator 功能,所以当你安装了 dotnetCampus.Ipc NuGet 包 后,这些代码将自动生成,省去了手工编写的费神。

dotnetCampus.Ipc 简介

例如你有一个接口 IWalterlv 和其对应的实现 WalterlvImpl

1 2 3 4 5 6 7 8 9 10 11 12

public interface IWalterlv { Task<string> GetUrlAsync(); } public class WalterlvImpl : IWalterlv { public Task<string> GetUrlAsync() { return Task.FromResult("https://blog.walterlv.com"); } }

那么只需要在 WalterlvImpl 上标记这是一个 IPC 对象即可:

1 2

++ IpcPublic(typeof(IWalterlv)) public class WalterlvImpl : IWalterlv

这时,编译这个项目,将会自动生成这样的两个类:

  • WalterlvIpcProxy:负责代理访问 IPC 对方
  • WalterlvIpcJoint:负责接收对方的 IPC 访问,然后对接到本地真实实例

那么本文就以它为例子说明如何编写一个代码生成器:

  1. 开始编写一个基本的代码生成器
  2. 使用代码生成器生成需要的代码
  3. 将代码生成器加入到现有的 NuGet 包中
  4. 调试代码生成器

一个基本的代码生成器

创建一个项目,例如 dotnetCampus.Ipc.Analyzers,然后编辑其项目文件(csproj)。至少要包含以下内容:

  • TargetFramework 必须是 netstandard2.0,目前(Visual Studio 2022 和 MSBuild 17)不支持其他任何框架。
  • 引用 Microsoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.CSharp 并且不对外传递他们的依赖。

1 2 3 4 5 6 7 8 9 10 11 12 13

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" /> </ItemGroup> </Project>

这里的 AppendTargetFrameworkToOutputPath 是可选的,目的是去掉生成路径下的 netstandard2.0 文件夹。

接着创建一个代码生成器类:

1 2 3 4 5 6 7 8 9 10 11

Generator public class ProxyJointGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { } public void Execute(GeneratorExecutionContext context) { } }

这样,你就写好了一个基本的生成器的代码框架了,剩下的就是往里面填内容了。

生成代码

Initialize 方法可进行一些初始化,你可以在这里订阅代码的变更通知,可以要求监听某些 C# 甚至是非代码文件的修改。本文是入门向,所以不涉及到这个方法。

接下来我们大部分的代码都将从那个 Execute 方法开始。

例如,我们可以随便写一个:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// 这段代码来自 https://docs.microsoft.com/zh-cn/dotnet/csharp/roslyn-sdk/source-generators-overview public void Execute(GeneratorExecutionContext context) { // find the main method var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken); // build up the source code string source = $@" using System; namespace {mainMethod.ContainingNamespace.ToDisplayString()} {{ public static partial class {mainMethod.ContainingType.Name} {{ static partial void HelloFrom(string name) {{ Console.WriteLine($""Generator says: Hi from '{{name}}'""); }} }} }} "; // add the source code to the compilation context.AddSource("generatedSource", source); }

这里的 AddSource 就是将代码添加到你的项目中了。

而我在 dotnetCampus.Ipc 库中编写的生成代码会稍微复杂一点,会根据项目中标记了 IpcPublic 的类的代码动态生成对这个类的代理访问和对接代码,使用的是 Roslyn 进行语义分析。可参见:使用 Roslyn 对 C# 代码进行语义分析 - walterlv

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

public void Execute(GeneratorExecutionContext context) { foreach (var ipcObjectType in FindIpcPublicObjects(context.Compilation)) { try { var contractType = ipcObjectType.ContractType; var proxySource = GenerateProxySource(ipcObjectType); var jointSource = GenerateJointSource(ipcObjectType); var assemblySource = GenerateAssemblyInfoSource(ipcObjectType); context.AddSource($"{contractType.Name}.proxy", SourceText.From(proxySource, Encoding.UTF8)); context.AddSource($"{contractType.Name}.joint", SourceText.From(jointSource, Encoding.UTF8)); context.AddSource($"{contractType.Name}.assembly", SourceText.From(assemblySource, Encoding.UTF8)); } catch (DiagnosticException ex) { context.ReportDiagnostic(ex.ToDiagnostic()); } catch (Exception ex) { context.ReportDiagnostic(Diagnostic.Create(DIPC001_UnknownError, null, ex)); } } }

这段代码的含义为:

  1. 通过自己写的 FindIpcPublicObjects 方法找到目前项目里所有的标记了 IpcPublic 特性的类;
  2. 为这个类生成代理类(Proxy);
  3. 为这个类生成对接类(Joint);
  4. 为这些类生成关系(AssemblyInfo);
  5. 将这些新生成的代码都加入到项目中进行编译;
  6. 如果中间出现了未知异常,则用自己编写的 DiagnosticException 异常类辅助报告编译错误。

这里只介绍创建代码分析器的一般方法,更多生成器代码可以前往仓库浏览:dotnetCampus.Ipc 项目

为 NuGet 包添加生成代码的功能

现在,我们要将这个生成代码的功能添加到 NuGet 包中。最终打出的 NuGet 包会是下面这样:

为了生成这样的包,我们需要:

  1. 添加解决方案依赖,确保编译 dotnetCampus.Ipc 之前,dotnetCampus.Ipc.Analyzers 项目已完成编译;
  2. 将 dotnetCampus.Ipc.Analyzers.dll 加入到 NuGet 包中。

对于 1,在解决方案上右键->“项目依赖项”,然后在 dotnetCampus.Ipc 项目上把 dotnetCampus.Ipc.Analyzers 勾上。

对于 2,我们需要修改真正打包的那个项目,也就是 dotnetCampus.Ipc 项目,在其 csproj 文件的末尾添加:

1 2 3 4 5

<Target Name="\_IncludeAllDependencies" BeforeTargets="\_GetPackageFiles"> <ItemGroup> <None Include="..\dotnetCampus.Ipc.Analyzers\bin\$(Configuration)\\*\*\\*.dll" Pack="True" PackagePath="analyzers\dotnet\cs" /> </ItemGroup> </Target>

这样便能生成我们期望的 NuGet 包了。等打包发布后,就能出现本文一开始说的能生成代码的效果了。

调试代码生成器

代码生成器编写更复杂的时候,调试就成了一个问题。接下来我们说说如何调试代码生成器。

这种代码的调试,大家可能很容易就想到了用 Debugger.Launch() 来调试,就像这样:

1 2 3 4

public void Initialize(GeneratorInitializationContext context) { ++ System.Diagnostics.Debugger.Launch(); }

但是,用什么项目的编译来触发这个调试呢?总不可能在某个项目上安装上这个 NuGet 包吧……那样效率太低了。

我们再建一个 dotnetCampus.Ipc.Test 项目,在其 csproj 文件上加上这么一行:

1 2 3

<ItemGroup> <ProjectReference Include="..\..\src\dotnetCampus.Ipc.Analyzers\dotnetCampus.Ipc.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup>

OutputItemType="Analyzer" 表示将项目添加为分析器,ReferenceOutputAssembly="false" 表示此项目无需引用分析器项目的程序集。

这样,编译此 dotnetCampus.Ipc.Test 项目时,就会触发选择调试器的界面,你就能调试你的代码生成器了。

使用这种方式引用,相比于 NuGet 包引用来说,项目的分析器列表里无法看到生成的代码。如果需要在这种情况下看到代码,你可能需要在 context.AddSource 那里打上一个断点,来看生成的代码是什么样的。

当然,除了用项目引用的方式,你还能直接引用最终的 dll:

1 2 3

<ItemGroup> <Analyzer Include="..\..\src\dotnetCampus.Ipc.Analyzers\bin\$(Configuration)\dotnetCampus.Ipc.Analyzers.dll" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup>

参考资料

本文会经常更新,请阅读原文: https://cloud.tencent.com/developer/article/2349830 ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://blog.walterlv.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系 ([email protected])

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • dotnetCampus.Ipc 简介
  • 一个基本的代码生成器
  • 生成代码
  • 为 NuGet 包添加生成代码的功能
  • 调试代码生成器
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档