云筑网技术团队
助推建筑行业数字化
摘要
元数据分析
使用 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 SourceGenerators
/// </summary>
[ ]
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>
[ ]
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}")],这样在运行时就会自动替换成对应的值。
新建 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);
}
}
}
}
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();
}
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));
...
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);
}
}
}
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());
...
}
[ ]
public interface IJsonServerApi
{
[ ]
Task<Todo> Get(int id, CancellationToken cancellationToken = default);
[ ]
Task<Todo> Post(CreateTodo createTodo, CancellationToken cancellationToken = default);
[ ]
Task<Todo> Put(Todo todo, CancellationToken cancellationToken);
[ ]
Task<Todo> Patch(int id, Todo todo, CancellationToken cancellationToken);
[ ]
Task<object> Delete(int id, CancellationToken cancellationToken);
}
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;
}
[ ]
public async Task<Todo> Get(int id, CancellationToken cancellationToken)
{
return await _jsonServerApi.Get(id, cancellationToken);
}
...
}
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