.NET SourceGenerators 根据 HTTPAPI 接口自动生成实现类
目录
- 摘要
- 元数据分析
- 使用 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:
/// <summary>
/// Identity a Interface which will be implemented by SourceGenerator
/// </summary>
[AttributeUsage(AttributeTargets.Interface)]
public class HttpClientAttribute : Attribute
{
/// <summary>
/// HttpClient name
/// </summary>
public string Name { get; }
/// <summary>
/// Create a new <see cref="HttpClientAttribute"/>
/// </summary>
public HttpClientAttribute()
{
}
/// <summary>
/// Create a new <see cref="HttpClientAttribute"/> with given name
/// </summary>
/// <param name="name"></param>
public HttpClientAttribute(string name)
{
Name = name;
}
}
然后我们定义接口方法上的注解,表明以何种方式请求 API 以及请求的模板路径,这里以HttpGet方法为例:
/// <summary>
/// Identity a method send HTTP Get request
/// </summary>
public class HttpGetAttribute : HttpMethodAttribute
{
/// <summary>
/// Creates a new <see cref="HttpGetAttribute"/> with the given route template.
/// </summary>
/// <param name="template">route template</param>
public HttpGetAttribute(string template) : base(template)
{
}
}
/// <summary>
/// HTTP method abstract type for common encapsulation
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public abstract class HttpMethodAttribute : Attribute
{
/// <summary>
/// Route template
/// </summary>
private string Template { get; }
/// <summary>
/// Creates a new <see cref="HttpMethodAttribute"/> with the given route template.
/// </summary>
/// <param name="template">route template</param>
protected HttpMethodAttribute(string template)
{
Template = template;
}
}
当然还提供RequiredServiceAttribute来注入服务,HeaderAttribute来添加头信息等注解这里不做展开,得益于 C# 的字符串插值(String interpolation)语法糖,要支持路由变量等功能,只需要用{}包裹变量就行 例如[HttpGet("/todos/{id}")],这样在运行时就会自动替换成对应的值。
使用 Source generators 实现
新建 HttpClient.SourceGenerator 项目,SourceGenerator 需要引入 Microsoft.CodeAnalysis.Analyzers, Microsoft.CodeAnalysis.CSharp 包,并将 TargetFramework 设置成 netstandard2.0。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
...
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
...
</ItemGroup>
</Project>
要使用 SourceGenerator 需要实现 ISourceGenerator 接口,并添加 [Generator] 注解,一般情况下我们在 Initialize 注册 Syntax receiver,将需要的类添加到接收器中,在 Execute 丢弃掉不是该接收器的上下文,执行具体的代码生成逻辑。
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
这里我们需要了解下 roslyn api 中的 语法树模型 (SyntaxTree model) 和 语义模型 (Semantic model),简单的讲, 语法树表示源代码的语法和词法结构,表明节点是接口声明还是类声明还是 using 指令块等等,这一部分信息来源于编译器的Parse阶段;语义来源于编译器的Declaration阶段,由一系列 Named symbol 构成,比如TypeSymbol,MethodSymbol等,类似于 CLR 类型系统, TypeSymbol 可以得到标记的注解信息,MethodSymbol 可以得到 ReturnType 等信息。
定义 HttpClient Syntax Receiver,这里我们处理节点信息是接口声明语法的节点,并且接口声明语法上有注解,然后再获取其语义模型,根据语义模型判断是否包含我们上边定义的 HttpClientAttribute。
class HttpClientSyntax : ISyntaxContextReceiver
{
public List<INamedTypeSymbol> TypeSymbols { get; set; } = new List<INamedTypeSymbol>();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is InterfaceDeclarationSyntax ids && ids.AttributeLists.Count > 0)
{
var typeSymbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, ids) as INamedTypeSymbol;
if (typeSymbol!.GetAttributes().Any(x =>
x.AttributeClass!.ToDisplayString() ==
"SourceGeneratorPower.HttpClient.HttpClientAttribute"))
{
TypeSymbols.Add(typeSymbol);
}
}
}
}
接下来就是循环处理接收器中的 TypeSymbol,根据接口里面定义的方法以及注解自动生成实现体的方法,这里不做展开详细代码可以查看 Github。
private string GenerateGetMethod(ITypeSymbol typeSymbol, IMethodSymbol methodSymbol, string httpClientName,
string requestUri)
{
var returnType = (methodSymbol.ReturnType as INamedTypeSymbol).TypeArguments[0].ToDisplayString();
var cancellationToken = methodSymbol.Parameters.Last().Name;
var source = GenerateHttpClient(typeSymbol, methodSymbol, httpClientName);
source.AppendLine($@"var response = await httpClient.GetAsync($""{requestUri}"", {cancellationToken});");
source.AppendLine("response!.EnsureSuccessStatusCode();");
source.AppendLine(
$@"return (await response.Content.ReadFromJsonAsync<{returnType}>(cancellationToken: {cancellationToken})!)!;");
source.AppendLine("}");
return source.ToString();
}
我们这里生成一个扩展方法,并将 HTTP API 接口和实现类添加到 DI容器,然后在主项目中调用这个扩展方法,同时为了避免可能的命名空间冲突,我们这里使用 global:: 加上包含命名空间的全名来引用。
var extensionSource = new StringBuilder($@"
using SourceGeneratorPower.HttpClient;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{{
public static class ScanInjectOptions
{{
public static void AddGeneratedHttpClient(this IServiceCollection services)
{{
");
foreach (var typeSymbol in receiver.TypeSymbols)
{
...
extensionSource.AppendLine(
$@"services.AddScoped<global::{typeSymbol.ToDisplayString()}, global::{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name.Substring(1)}>();");
}
extensionSource.AppendLine("}}}");
var extensionTextFormatted = CSharpSyntaxTree
.ParseText(extensionSource.ToString(), new CSharpParseOptions(LanguageVersion.CSharp8)).GetRoot()
.NormalizeWhitespace().SyntaxTree.GetText().ToString();
context.AddSource($"SourceGeneratorPower.HttpClientExtension.AutoGenerated.cs",
SourceText.From(extensionTextFormatted, Encoding.UTF8));
...
使用 Source generators 实现程序集分析
在上面我们介绍了如何根据语法树来分析哪些接口需要生成这只适合单项目,但在实际工作中常常是分项目开发的,项目之间通过 ProjectReference 引用。
在 Source generators 中我们可以使用 context.Compilation.SourceModule.ReferencedAssemblySymbols 来分析程序集中的代码,这其中包含了框架的引用程序集,项目引用的程序集以及 nuget 包引用的程序集,我们可以通过 PublicKey 为空条件只保留项目引用的程序集。
在程序集符号(IAssemblySymbol)中, 符号的关系如下图,我们需要的是找到最终的 INameTypeSymbol 判断是否是需要我们进行生成的接口。

这里我们可以自定义 Symbol visitor 来实现遍历扫描需要生成的接口。
class HttpClientVisitor : SymbolVisitor
{
private readonly HashSet<INamedTypeSymbol> _httpClientTypeSymbols;
public HttpClientVisitor()
{
_httpClientTypeSymbols = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
}
public ImmutableArray<INamedTypeSymbol> GetHttpClientTypes() => _httpClientTypeSymbols.ToImmutableArray();
public override void VisitAssembly(IAssemblySymbol symbol)
{
symbol.GlobalNamespace.Accept(this);
}
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var namespaceOrTypeSymbol in symbol.GetMembers())
{
namespaceOrTypeSymbol.Accept(this);
}
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (symbol.DeclaredAccessibility != Accessibility.Public)
{
return;
}
if (symbol.GetAttributes().Any(x =>
x.AttributeClass!.ToDisplayString() == "SourceGeneratorPower.HttpClient.HttpClientAttribute"))
{
_httpClientTypeSymbols.Add(symbol);
}
var nestedTypes = symbol.GetMembers();
if (nestedTypes.IsDefaultOrEmpty)
{
return;
}
foreach (var nestedType in nestedTypes)
{
nestedType.Accept(this);
}
}
}
然后将这部分与上边的 HttpClientSymbolReceiver的 INameTypeSymbol 合并到一起,生成代码的逻辑不变。
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxContextReceiver is HttpClientSyntax receiver))
{
return;
}
var httpClientVisitor = new HttpClientVisitor();
foreach (var assemblySymbol in context.Compilation.SourceModule.ReferencedAssemblySymbols
.Where(x => x.Identity.PublicKey == ImmutableArray<byte>.Empty))
{
assemblySymbol.Accept(httpClientVisitor);
}
receiver.TypeSymbols.AddRange(httpClientVisitor.GetHttpClientTypes());
...
}
使用方法
接口定义
[HttpClient("JsonServer")]
public interface IJsonServerApi
{
[HttpGet("/todos/{id}")]
Task<Todo> Get(int id, CancellationToken cancellationToken = default);
[HttpPost(("/todos"))]
Task<Todo> Post(CreateTodo createTodo, CancellationToken cancellationToken = default);
[HttpPut("/todos/{todo.Id}")]
Task<Todo> Put(Todo todo, CancellationToken cancellationToken);
[HttpPatch("/todos/{id}")]
Task<Todo> Patch(int id, Todo todo, CancellationToken cancellationToken);
[HttpDelete("/todos/{id}")]
Task<object> Delete(int id, CancellationToken cancellationToken);
}
主项目引用,并配置对应的 HttpClient
builder.Services.AddGeneratedHttpClient();
builder.Services.AddHttpClient("JsonServer", options => options.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
注入接口并使用
public class TodoController: ControllerBase
{
private readonly IJsonServerApi _jsonServerApi;
public TodoController(IJsonServerApi jsonServerApi)
{
_jsonServerApi = jsonServerApi;
}
[HttpGet("{id}")]
public async Task<Todo> Get(int id, CancellationToken cancellationToken)
{
return await _jsonServerApi.Get(id, cancellationToken);
}
...
}
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 接口自动生成实现类的更多相关文章
- Mybatis自动生成实体类、dao接口和mapping映射文件
由于Mybatis是一种半自动的ORM框架,它的工作主要是配置mapping映射文件,为了减少手动书写映射文件,可以利用mybatis生成器,自动生成实体类.dao接口以及它的映射文件,然后直接拷贝到 ...
- Mybatis自动生成实体类
Maven自动生成实体类需要的jar包 一.pom.xml中 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns ...
- Springboot mybatis generate 自动生成实体类和Mapper
https://github.com/JasmineQian/SpringDemo_2019/tree/master/mybatis Springboot让java开发变得方便,Springboot中 ...
- 使用T4为数据库自动生成实体类
T4 (Text Template Transformation Toolkit) 是一个基于模板的代码生成器.使用T4你可以通过写一些ASP.NET-like模板,来生成C#, T-SQL, XML ...
- ANTLR和StringTemplate实例:自动生成单元测试类
ANTLR和StringTemplate实例:自动生成单元测试类 1. ANTLR语法 要想自动生成单元测试,首先第一步就是分析被测试类.这里以Java代码为例,用ANTLR对Java代码进行分析.要 ...
- 【原创】有关Silverlight中自动生成的类中 没有WCF层edmx模型新加入的对象 原因分析。
前端页面层: 编译老是不通过,报如下如所示错误: -- 然后下意识的查了下 生成的cs文件,没有搜到根据edmx 生成的 对应的类. 结果整理: 1.尽管在 edmx 模 ...
- mybatis怎样自动生成java类,配置文件?
其实没有什么东西是可以自动生成的,只不过是别人已经写好了,你调用罢了. 所以想要mybatis自动生成java类,配置文件等,就必须要一些配置和一些jar包.当然这些配置也很简单. 为了有个初步的认识 ...
- .net core 中简单封装Dapper.Extensions 并使用sqlsuger自动生成实体类
引言 由公司需要使用dapper 同时支持多数据库 又需要支持实体类 又需要支持sql 还需要支持事务 所以采用了 dapper + dapperExtensions 并配套 生成实体类小工具的方 ...
- Asp.Net Core如何根据数据库自动生成实体类
通过引用Nuget包添加实体类 运行 Install-Package Microsoft.EntityFrameworkCore.SqlServer 运行 Install-Package Micros ...
随机推荐
- 🍃【Spring专题】「原理系列」SpringMVC的运行工作原理(补充修订)
承接相关之前的SpringMVC的框架技术的流程分析 初始化流程(initStrategies) 执行流程 寻找相关HandlerMapping 请求到DispatcherServlet类进行执行相关 ...
- 【LeetCode】111. Minimum Depth of Binary Tree 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 DFS BFS 日期 [LeetCode] 题目地址 ...
- 【LeetCode】256. Paint House 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 动态规划 日期 题目地址:https://leetco ...
- 【LeetCode】113. Path Sum II 路径总和 II 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.me/ 文章目录 题目描述 题目大意 解题方法 BFS DFS 日期 题目地址:https:// ...
- Rectangles(hdu2461)
Rectangles Time Limit: 5000/4000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total ...
- Python Revisited Day10 (进程与线程)
目录 10.1 使用多进程模块 10.2 将工作分布到多个线程 <Python 3 程序开发指南>学习笔记 有俩种方法可以对工作载荷进行分布,一种是使用多进程,另一种是使用多线程. 10. ...
- 【Azure 应用服务】探索在Azure上设置禁止任何人访问App Service的默认域名(Default URL)
问题描述 总所周知,Azure App Service服务会默认提供一个 ***.chinacloudsites.cn为后缀的域名,但是该域名由上海蓝云网络科技有限公司备案,仅用于向其客户提供 Azu ...
- MySQL高级查询与编程笔记 • 【第5章 常见数据库对象】
全部章节 >>>> 本章目录 5.1 视图 5.1.1 视图的定义 5.1.2 视图的优点 5.1.3 视图的创建和使用 5.1.4 利用视图解决数据库的复杂应用 5.1. ...
- Ranger开源贡献统计
统计一下自己在Ranger开源社区贡献的Issue数量, 开源社区的Issue主要分为New Feature,Bug,Improvement, 这三种都是和代码相关的,会直接修改开源项目的代码库, 还 ...
- 什么是UE模型?
书接上文:不知怎么选,用RFM模型看舔狗质量! 这里要注意一个问题,我这里是因为内部信息敏感,才抽象成舔狗,大家不要以为我真的在说舔狗...... UE模型即Unit Economics,是指单体经济 ...