目录

  • 摘要
  • 元数据分析
  • 使用 Source generators 实现
  • 使用 Source generators 实现程序集分析
  • 使用方法
  • SourceCode && Nuget package
  • 总结

摘要

Source generators 随着 .net5 推出,并在 .net6 中大量运用,它可以基于编译时分析,根据现有代码创建新的代码并添加进编译时。利用 SourceGenerator 可以将开发人员从一些模板化的重复的工作中解放出来,更多的投入创造力的工作,并且和原生代码一致的性能。 在这篇文章中,我们将演示如何使用 Source generators 根据 HTTP API 接口自动生成实现类,以及实现跨项目分析,并且添加进 DI 容器。

元数据分析

Source generators 可以根据编译时语法树(Syntax)或符号(Symbol)分析,来执行创建新代码,因此我们需要在编译前提供足够多的元数据,在本文中我们需要知道哪些接口需要生成实现类,并且接口中定义的方法该以 Get,Post 等哪种方法发送出去,在本文中我们通过注解(Attribute/Annotation)来提供这些元数据,当然您也可以通过接口约束,命名惯例来提供。

首先我们定义接口上的注解,这将决定我们需要扫描的接口以及如何创建 HttpClient:

  1. /// <summary>
  2. /// Identity a Interface which will be implemented by SourceGenerator
  3. /// </summary>
  4. [AttributeUsage(AttributeTargets.Interface)]
  5. public class HttpClientAttribute : Attribute
  6. {
  7. /// <summary>
  8. /// HttpClient name
  9. /// </summary>
  10. public string Name { get; }
  11. /// <summary>
  12. /// Create a new <see cref="HttpClientAttribute"/>
  13. /// </summary>
  14. public HttpClientAttribute()
  15. {
  16. }
  17. /// <summary>
  18. /// Create a new <see cref="HttpClientAttribute"/> with given name
  19. /// </summary>
  20. /// <param name="name"></param>
  21. public HttpClientAttribute(string name)
  22. {
  23. Name = name;
  24. }
  25. }

然后我们定义接口方法上的注解,表明以何种方式请求 API 以及请求的模板路径,这里以HttpGet方法为例:

  1. /// <summary>
  2. /// Identity a method send HTTP Get request
  3. /// </summary>
  4. public class HttpGetAttribute : HttpMethodAttribute
  5. {
  6. /// <summary>
  7. /// Creates a new <see cref="HttpGetAttribute"/> with the given route template.
  8. /// </summary>
  9. /// <param name="template">route template</param>
  10. public HttpGetAttribute(string template) : base(template)
  11. {
  12. }
  13. }
  14. /// <summary>
  15. /// HTTP method abstract type for common encapsulation
  16. /// </summary>
  17. [AttributeUsage(AttributeTargets.Method)]
  18. public abstract class HttpMethodAttribute : Attribute
  19. {
  20. /// <summary>
  21. /// Route template
  22. /// </summary>
  23. private string Template { get; }
  24. /// <summary>
  25. /// Creates a new <see cref="HttpMethodAttribute"/> with the given route template.
  26. /// </summary>
  27. /// <param name="template">route template</param>
  28. protected HttpMethodAttribute(string template)
  29. {
  30. Template = template;
  31. }
  32. }

当然还提供RequiredServiceAttribute来注入服务,HeaderAttribute来添加头信息等注解这里不做展开,得益于 C# 的字符串插值(String interpolation)语法糖,要支持路由变量等功能,只需要用{}包裹变量就行 例如[HttpGet("/todos/{id}")],这样在运行时就会自动替换成对应的值。

使用 Source generators 实现

新建 HttpClient.SourceGenerator 项目,SourceGenerator 需要引入 Microsoft.CodeAnalysis.Analyzers, Microsoft.CodeAnalysis.CSharp 包,并将 TargetFramework 设置成 netstandard2.0。

  1. <Project Sdk="Microsoft.NET.Sdk">
  2. <PropertyGroup>
  3. <TargetFramework>netstandard2.0</TargetFramework>
  4. <IncludeBuildOutput>false</IncludeBuildOutput>
  5. ...
  6. </PropertyGroup>
  7. <ItemGroup>
  8. <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
  9. <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
  10. <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  11. ...
  12. </ItemGroup>
  13. </Project>

要使用 SourceGenerator 需要实现 ISourceGenerator 接口,并添加 [Generator] 注解,一般情况下我们在 Initialize 注册 Syntax receiver,将需要的类添加到接收器中,在 Execute 丢弃掉不是该接收器的上下文,执行具体的代码生成逻辑。

  1. public interface ISourceGenerator
  2. {
  3. void Initialize(GeneratorInitializationContext context);
  4. void Execute(GeneratorExecutionContext context);
  5. }

这里我们需要了解下 roslyn api 中的 语法树模型 (SyntaxTree model) 和 语义模型 (Semantic model),简单的讲, 语法树表示源代码的语法和词法结构,表明节点是接口声明还是类声明还是 using 指令块等等,这一部分信息来源于编译器的Parse阶段;语义来源于编译器的Declaration阶段,由一系列 Named symbol 构成,比如TypeSymbolMethodSymbol等,类似于 CLR 类型系统, TypeSymbol 可以得到标记的注解信息,MethodSymbol 可以得到 ReturnType 等信息。

定义 HttpClient Syntax Receiver,这里我们处理节点信息是接口声明语法的节点,并且接口声明语法上有注解,然后再获取其语义模型,根据语义模型判断是否包含我们上边定义的 HttpClientAttribute

  1. class HttpClientSyntax : ISyntaxContextReceiver
  2. {
  3. public List<INamedTypeSymbol> TypeSymbols { get; set; } = new List<INamedTypeSymbol>();
  4. public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
  5. {
  6. if (context.Node is InterfaceDeclarationSyntax ids && ids.AttributeLists.Count > 0)
  7. {
  8. var typeSymbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, ids) as INamedTypeSymbol;
  9. if (typeSymbol!.GetAttributes().Any(x =>
  10. x.AttributeClass!.ToDisplayString() ==
  11. "SourceGeneratorPower.HttpClient.HttpClientAttribute"))
  12. {
  13. TypeSymbols.Add(typeSymbol);
  14. }
  15. }
  16. }
  17. }

接下来就是循环处理接收器中的 TypeSymbol,根据接口里面定义的方法以及注解自动生成实现体的方法,这里不做展开详细代码可以查看 Github。

  1. private string GenerateGetMethod(ITypeSymbol typeSymbol, IMethodSymbol methodSymbol, string httpClientName,
  2. string requestUri)
  3. {
  4. var returnType = (methodSymbol.ReturnType as INamedTypeSymbol).TypeArguments[0].ToDisplayString();
  5. var cancellationToken = methodSymbol.Parameters.Last().Name;
  6. var source = GenerateHttpClient(typeSymbol, methodSymbol, httpClientName);
  7. source.AppendLine($@"var response = await httpClient.GetAsync($""{requestUri}"", {cancellationToken});");
  8. source.AppendLine("response!.EnsureSuccessStatusCode();");
  9. source.AppendLine(
  10. $@"return (await response.Content.ReadFromJsonAsync<{returnType}>(cancellationToken: {cancellationToken})!)!;");
  11. source.AppendLine("}");
  12. return source.ToString();
  13. }

我们这里生成一个扩展方法,并将 HTTP API 接口和实现类添加到 DI容器,然后在主项目中调用这个扩展方法,同时为了避免可能的命名空间冲突,我们这里使用 global:: 加上包含命名空间的全名来引用。

  1. var extensionSource = new StringBuilder($@"
  2. using SourceGeneratorPower.HttpClient;
  3. using Microsoft.Extensions.Configuration;
  4. namespace Microsoft.Extensions.DependencyInjection
  5. {{
  6. public static class ScanInjectOptions
  7. {{
  8. public static void AddGeneratedHttpClient(this IServiceCollection services)
  9. {{
  10. ");
  11. foreach (var typeSymbol in receiver.TypeSymbols)
  12. {
  13. ...
  14. extensionSource.AppendLine(
  15. $@"services.AddScoped<global::{typeSymbol.ToDisplayString()}, global::{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name.Substring(1)}>();");
  16. }
  17. extensionSource.AppendLine("}}}");
  18. var extensionTextFormatted = CSharpSyntaxTree
  19. .ParseText(extensionSource.ToString(), new CSharpParseOptions(LanguageVersion.CSharp8)).GetRoot()
  20. .NormalizeWhitespace().SyntaxTree.GetText().ToString();
  21. context.AddSource($"SourceGeneratorPower.HttpClientExtension.AutoGenerated.cs",
  22. SourceText.From(extensionTextFormatted, Encoding.UTF8));
  23. ...

使用 Source generators 实现程序集分析

在上面我们介绍了如何根据语法树来分析哪些接口需要生成这只适合单项目,但在实际工作中常常是分项目开发的,项目之间通过 ProjectReference 引用。

在 Source generators 中我们可以使用 context.Compilation.SourceModule.ReferencedAssemblySymbols 来分析程序集中的代码,这其中包含了框架的引用程序集,项目引用的程序集以及 nuget 包引用的程序集,我们可以通过 PublicKey 为空条件只保留项目引用的程序集。

在程序集符号(IAssemblySymbol)中, 符号的关系如下图,我们需要的是找到最终的 INameTypeSymbol 判断是否是需要我们进行生成的接口。

这里我们可以自定义 Symbol visitor 来实现遍历扫描需要生成的接口。

  1. class HttpClientVisitor : SymbolVisitor
  2. {
  3. private readonly HashSet<INamedTypeSymbol> _httpClientTypeSymbols;
  4. public HttpClientVisitor()
  5. {
  6. _httpClientTypeSymbols = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
  7. }
  8. public ImmutableArray<INamedTypeSymbol> GetHttpClientTypes() => _httpClientTypeSymbols.ToImmutableArray();
  9. public override void VisitAssembly(IAssemblySymbol symbol)
  10. {
  11. symbol.GlobalNamespace.Accept(this);
  12. }
  13. public override void VisitNamespace(INamespaceSymbol symbol)
  14. {
  15. foreach (var namespaceOrTypeSymbol in symbol.GetMembers())
  16. {
  17. namespaceOrTypeSymbol.Accept(this);
  18. }
  19. }
  20. public override void VisitNamedType(INamedTypeSymbol symbol)
  21. {
  22. if (symbol.DeclaredAccessibility != Accessibility.Public)
  23. {
  24. return;
  25. }
  26. if (symbol.GetAttributes().Any(x =>
  27. x.AttributeClass!.ToDisplayString() == "SourceGeneratorPower.HttpClient.HttpClientAttribute"))
  28. {
  29. _httpClientTypeSymbols.Add(symbol);
  30. }
  31. var nestedTypes = symbol.GetMembers();
  32. if (nestedTypes.IsDefaultOrEmpty)
  33. {
  34. return;
  35. }
  36. foreach (var nestedType in nestedTypes)
  37. {
  38. nestedType.Accept(this);
  39. }
  40. }
  41. }

然后将这部分与上边的 HttpClientSymbolReceiver的 INameTypeSymbol 合并到一起,生成代码的逻辑不变。

  1. public void Execute(GeneratorExecutionContext context)
  2. {
  3. if (!(context.SyntaxContextReceiver is HttpClientSyntax receiver))
  4. {
  5. return;
  6. }
  7. var httpClientVisitor = new HttpClientVisitor();
  8. foreach (var assemblySymbol in context.Compilation.SourceModule.ReferencedAssemblySymbols
  9. .Where(x => x.Identity.PublicKey == ImmutableArray<byte>.Empty))
  10. {
  11. assemblySymbol.Accept(httpClientVisitor);
  12. }
  13. receiver.TypeSymbols.AddRange(httpClientVisitor.GetHttpClientTypes());
  14. ...
  15. }

使用方法

接口定义

  1. [HttpClient("JsonServer")]
  2. public interface IJsonServerApi
  3. {
  4. [HttpGet("/todos/{id}")]
  5. Task<Todo> Get(int id, CancellationToken cancellationToken = default);
  6. [HttpPost(("/todos"))]
  7. Task<Todo> Post(CreateTodo createTodo, CancellationToken cancellationToken = default);
  8. [HttpPut("/todos/{todo.Id}")]
  9. Task<Todo> Put(Todo todo, CancellationToken cancellationToken);
  10. [HttpPatch("/todos/{id}")]
  11. Task<Todo> Patch(int id, Todo todo, CancellationToken cancellationToken);
  12. [HttpDelete("/todos/{id}")]
  13. Task<object> Delete(int id, CancellationToken cancellationToken);
  14. }

主项目引用,并配置对应的 HttpClient

  1. builder.Services.AddGeneratedHttpClient();
  2. builder.Services.AddHttpClient("JsonServer", options => options.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));

注入接口并使用

  1. public class TodoController: ControllerBase
  2. {
  3. private readonly IJsonServerApi _jsonServerApi;
  4. public TodoController(IJsonServerApi jsonServerApi)
  5. {
  6. _jsonServerApi = jsonServerApi;
  7. }
  8. [HttpGet("{id}")]
  9. public async Task<Todo> Get(int id, CancellationToken cancellationToken)
  10. {
  11. return await _jsonServerApi.Get(id, cancellationToken);
  12. }
  13. ...
  14. }

SourceCode && Nuget package

SourceCode: https://github.com/huiyuanai709/SourceGeneratorPower

Nuget Package: https://www.nuget.org/packages/SourceGeneratorPower.HttpClient.Abstractions

Nuget Package: https://www.nuget.org/packages/SourceGeneratorPower.HttpClient.SourceGenerator

总结

Source generators 非常强(Powerful!!!),以一种现代化的,人类可读(human readable)的方式解决重复编码的问题,并且拥有与原生代码一致的性能,读者可以结合文章以及官方示例用 Source generators 来解决实际工作中的问题,任何建议和新功能需求也欢迎留言或在 Github 上提出。

.NET SourceGenerators 根据 HTTPAPI 接口自动生成实现类的更多相关文章

  1. Mybatis自动生成实体类、dao接口和mapping映射文件

    由于Mybatis是一种半自动的ORM框架,它的工作主要是配置mapping映射文件,为了减少手动书写映射文件,可以利用mybatis生成器,自动生成实体类.dao接口以及它的映射文件,然后直接拷贝到 ...

  2. Mybatis自动生成实体类

    Maven自动生成实体类需要的jar包 一.pom.xml中 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns ...

  3. Springboot mybatis generate 自动生成实体类和Mapper

    https://github.com/JasmineQian/SpringDemo_2019/tree/master/mybatis Springboot让java开发变得方便,Springboot中 ...

  4. 使用T4为数据库自动生成实体类

    T4 (Text Template Transformation Toolkit) 是一个基于模板的代码生成器.使用T4你可以通过写一些ASP.NET-like模板,来生成C#, T-SQL, XML ...

  5. ANTLR和StringTemplate实例:自动生成单元测试类

    ANTLR和StringTemplate实例:自动生成单元测试类 1. ANTLR语法 要想自动生成单元测试,首先第一步就是分析被测试类.这里以Java代码为例,用ANTLR对Java代码进行分析.要 ...

  6. 【原创】有关Silverlight中自动生成的类中 没有WCF层edmx模型新加入的对象 原因分析。

      前端页面层:    编译老是不通过,报如下如所示错误:     -- 然后下意识的查了下 生成的cs文件,没有搜到根据edmx 生成的 对应的类.       结果整理: 1.尽管在 edmx 模 ...

  7. mybatis怎样自动生成java类,配置文件?

    其实没有什么东西是可以自动生成的,只不过是别人已经写好了,你调用罢了. 所以想要mybatis自动生成java类,配置文件等,就必须要一些配置和一些jar包.当然这些配置也很简单. 为了有个初步的认识 ...

  8. .net core 中简单封装Dapper.Extensions 并使用sqlsuger自动生成实体类

    引言 由公司需要使用dapper  同时支持多数据库 又需要支持实体类 又需要支持sql 还需要支持事务 所以采用了 dapper + dapperExtensions  并配套 生成实体类小工具的方 ...

  9. Asp.Net Core如何根据数据库自动生成实体类

    通过引用Nuget包添加实体类 运行 Install-Package Microsoft.EntityFrameworkCore.SqlServer 运行 Install-Package Micros ...

随机推荐

  1. Spring支持的常用数据库传播属性和事务隔离级别

    一.事务的传播属性 1.propagation:用来设置事务的传播行为,一个方法运行在了一个开启了事务的方法中时,当前方法是使用原来的事务还是开启一个新的事务. (1)propagation.REQU ...

  2. 【LeetCode】1051. Height Checker 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 排序比较 日期 题目地址:https://leetc ...

  3. 【九度OJ】题目1118:数制转换 解题报告

    [九度OJ]题目1118:数制转换 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1118 题目描述: 求任意两个不同进制非 ...

  4. 【LeetCode】355. Design Twitter 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  5. Counting Offspring(hdu3887)

    Counting Offspring Time Limit: 15000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Othe ...

  6. 2021 年终总结:内推40人、全网15万粉、Code Runner 3000万下载、发扬WLB、进军视频领域

    时光飞逝,岁月如梭,蓦然回首,已是年底. 感觉写 2020 年终总结还是在不久之前.转眼间,2021 已经接近尾声了.是时候来写写 2021 年的年终总结了. 内推 40 人 2019 年,内推了 2 ...

  7. 洛谷1052——过河(DP+状态压缩)

    题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数 ...

  8. 第十五个知识点:RSA-OAEP和ECIES的密钥生成,加密和解密

    第十五个知识点:RSA-OAEP和ECIES的密钥生成,加密和解密 1.RSA-OAEP RSA-OAEP是RSA加密方案和OAEP填充方案的同时使用.现实世界中它们同时使用.(这里介绍的只是&quo ...

  9. 如何优雅地读写HttpServletRequest和HttpServletResponse的请求体

    最近很多交互要同原生的HttpServletRequest和HttpServletResponse打交道.从HttpServletRequest中读取body数据封装成某种数据结构:向HttpServ ...

  10. CS5212 pin to pin 替代RTD2166|DP转VGA芯片|CS5212转换电路设计方法

    CS5212适用于设计DP转VGA转换电路,主要用在嵌入式单片机基于工业机或者INTEL X86主板上面,也适用于多个电子配件市场和显示器应用程序,如笔记本电脑.主板.台式机.适配器.转换器和转接器. ...