使用.NET源生成器(SG)实现一个自动注入的生成器
DI依赖注入对我们后端程序员来说肯定是基础中的基础了,我们经常会使用下面的代码注入相关的service
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService3>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services2.MyService>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService>();
services.AddSingleton<Biwen.AutoClassGen.TestConsole.Services.ITestService, Biwen.AutoClassGen.TestConsole.Services.TestService>();
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.ITest2Service, Biwen.AutoClassGen.TestConsole.Services.TestService>();
对于上面的代码如果代码量很大 而且随着项目的迭代可能会堆积更多的代码,对于很多程序员来说第一想到的可能是透过反射批量注入,当然这也是最简单最直接的方式,今天我们使用源生成器的方式实现这个功能, 使用源生成器的方式好处还是有的 比如AOT需求,极致性能要求
实现这个功能的具体步骤:
定义Attribute-标注Attribute-遍历代码中标注Attribute的metadata集合-生成源代码
首先我们定义一个Attribute用于标注需要注入的类
namespace Biwen.AutoClassGen.Attributes
{
using System;
/// <summary>
/// 服务生命周期
/// </summary>
public enum ServiceLifetime
{
Singleton = 1,
Transient = 2,
Scoped = 4,
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class AutoInjectAttribute : Attribute
{
public ServiceLifetime ServiceLifetime { get; set; }
public Type BaseType { get; set; }
/// <summary>
///
/// </summary>
/// <param name="baseType">NULL表示服务自身</param>
/// <param name="serviceLifetime">服务生命周期</param>
public AutoInjectAttribute(Type baseType = null, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped)
{
ServiceLifetime = serviceLifetime;
BaseType = baseType;
}
}
//C#11及以上的版本支持泛型Attribute
#if NET7_0_OR_GREATER
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class AutoInjectAttribute<T> : AutoInjectAttribute
{
public AutoInjectAttribute(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) : base(typeof(T), serviceLifetime)
{
}
}
#endif
}
通过上面定义的Attribute我们就可以给我们的服务打上标记了
[AutoInject<TestService>]
[AutoInject<ITestService>(ServiceLifetime.Singleton)]
[AutoInject<ITest2Service>(ServiceLifetime.Scoped)]
public class TestService : ITestService, ITest2Service
{
public string Say(string message)
{
return $"hello {message}";
}
public string Say2(string message)
{
return message;
}
}
[AutoInject]
[AutoInject(serviceLifetime: ServiceLifetime.Transient)]
[AutoInject(typeof(ITest2Service), ServiceLifetime.Scoped)]
public class TestService2 : ITest2Service
{
public string Say2(string message)
{
return message;
}
}
接下来就是Roslyn分析C#语法解析代码片段:
实现源生成器的唯一接口IIncrementalGenerator 实现Initialize方法:
private const string AttributeValueMetadataNameInject = "AutoInject";
/// <summary>
/// 泛型AutoInjectAttribute
/// </summary>
private const string GenericAutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute`1";
/// <summary>
/// 非泛型AutoInjectAttribute
/// </summary>
private const string AutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute";
#region 非泛型
//使用SyntaxProvider的ForAttributeWithMetadataName得到所有标注的服务集合
var nodesAutoInject = context.SyntaxProvider.ForAttributeWithMetadataName(
AutoInjectAttributeName,
(context, attributeSyntax) => true,
(syntaxContext, _) => syntaxContext.TargetNode).Collect();
IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInject =
context.CompilationProvider.Combine(nodesAutoInject);
#endregion
#region 泛型
var nodesAutoInjectG = context.SyntaxProvider.ForAttributeWithMetadataName(
GenericAutoInjectAttributeName,
(context, attributeSyntax) => true,
(syntaxContext, _) => syntaxContext.TargetNode).Collect();
IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxNode>)> compilationAndTypesInjectG =
context.CompilationProvider.Combine(nodesAutoInjectG);
#endregion
//合并所有的服务的编译类型
var join = compilationAndTypesInject.Combine(compilationAndTypesInjectG);
解下来我们定义一个Metadata类,该类主要定义Attribute的字段
private record AutoInjectDefine
{
public string ImplType { get; set; } = null!;
public string BaseType { get; set; } = null!;
public string LifeTime { get; set; } = null!;
}
解析所有的标注泛型的Attribute metadata
private static List<AutoInjectDefine> GetGenericAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
{
if (nodes.Length == 0) return [];
// 注册的服务
List<AutoInjectDefine> autoInjects = [];
List<string> namespaces = [];
foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
{
AttributeSyntax? attributeSyntax = null;
foreach (var attr in node.AttributeLists.AsEnumerable())
{
var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
attributeSyntax = attr.Attributes.First(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);
if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
{
//转译的Entity类名
var baseTypeName = string.Empty;
string pattern = @"(?<=<)(?<type>\w+)(?=>)";
var match = Regex.Match(attributeSyntax.ToString(), pattern);
if (match.Success)
{
baseTypeName = match.Groups["type"].Value.Split(['.']).Last();
}
else
{
continue;
}
var implTypeName = node.Identifier.ValueText;
//var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
var symbols = compilation.GetSymbolsWithName(implTypeName);
foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
{
implTypeName = symbol.ToDisplayString();
break;
}
var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
{
baseTypeName = baseSymbol.ToDisplayString();
break;
}
string lifeTime = "AddScoped"; //default
{
if (attributeSyntax.ArgumentList != null)
{
for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
{
var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
lifeTime = name switch
{
"Singleton" => "AddSingleton",
"Transient" => "AddTransient",
"Scoped" => "AddScoped",
_ => "AddScoped",
};
break;
}
}
}
autoInjects.Add(new AutoInjectDefine
{
ImplType = implTypeName,
BaseType = baseTypeName,
LifeTime = lifeTime,
});
//命名空间
symbols = compilation.GetSymbolsWithName(baseTypeName);
foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
{
var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
// 命名空间
if (!namespaces.Contains(fullNameSpace))
{
namespaces.Add(fullNameSpace);
}
}
}
}
}
}
return autoInjects;
}
解析所有标注非泛型Attribute的metadata集合
private static List<AutoInjectDefine> GetAnnotatedNodesInject(Compilation compilation, ImmutableArray<SyntaxNode> nodes)
{
if (nodes.Length == 0) return [];
// 注册的服务
List<AutoInjectDefine> autoInjects = [];
List<string> namespaces = [];
foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast<ClassDeclarationSyntax>())
{
AttributeSyntax? attributeSyntax = null;
foreach (var attr in node.AttributeLists.AsEnumerable())
{
var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString();
attributeSyntax = attr.Attributes.FirstOrDefault(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0);
//其他的特性直接跳过
if (attributeSyntax is null) continue;
if (attrName?.IndexOf(AttributeValueMetadataNameInject, System.StringComparison.Ordinal) == 0)
{
var implTypeName = node.Identifier.ValueText;
//var rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString();
var symbols = compilation.GetSymbolsWithName(implTypeName);
foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
{
implTypeName = symbol.ToDisplayString();
break;
}
//转译的Entity类名
var baseTypeName = string.Empty;
if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList!.Arguments.Count == 0)
{
baseTypeName = implTypeName;
}
else
{
if (attributeSyntax.ArgumentList!.Arguments[0].Expression is TypeOfExpressionSyntax)
{
var eType = (attributeSyntax.ArgumentList!.Arguments[0].Expression as TypeOfExpressionSyntax)!.Type;
if (eType.IsKind(SyntaxKind.IdentifierName))
{
baseTypeName = (eType as IdentifierNameSyntax)!.Identifier.ValueText;
}
else if (eType.IsKind(SyntaxKind.QualifiedName))
{
baseTypeName = (eType as QualifiedNameSyntax)!.ToString().Split(['.']).Last();
}
else if (eType.IsKind(SyntaxKind.AliasQualifiedName))
{
baseTypeName = (eType as AliasQualifiedNameSyntax)!.ToString().Split(['.']).Last();
}
if (string.IsNullOrEmpty(baseTypeName))
{
baseTypeName = implTypeName;
}
}
else
{
baseTypeName = implTypeName;
}
}
var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
foreach (ITypeSymbol baseSymbol in baseSymbols.Cast<ITypeSymbol>())
{
baseTypeName = baseSymbol.ToDisplayString();
break;
}
string lifeTime = "AddScoped"; //default
{
if (attributeSyntax.ArgumentList != null)
{
for (var i = 0; i < attributeSyntax.ArgumentList!.Arguments.Count; i++)
{
var expressionSyntax = attributeSyntax.ArgumentList.Arguments[i].Expression;
if (expressionSyntax.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
var name = (expressionSyntax as MemberAccessExpressionSyntax)!.Name.Identifier.ValueText;
lifeTime = name switch
{
"Singleton" => "AddSingleton",
"Transient" => "AddTransient",
"Scoped" => "AddScoped",
_ => "AddScoped",
};
break;
}
}
}
autoInjects.Add(new AutoInjectDefine
{
ImplType = implTypeName,
BaseType = baseTypeName,
LifeTime = lifeTime,
});
//命名空间
symbols = compilation.GetSymbolsWithName(baseTypeName);
foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>())
{
var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
// 命名空间
if (!namespaces.Contains(fullNameSpace))
{
namespaces.Add(fullNameSpace);
}
}
}
}
}
}
return autoInjects;
}
通过上面的两个方法我们就取到了所有的Attribute的metadata,接下来的代码其实就比较简单了 原理就是将metadata转换为形如以下的代码:
#pragma warning disable
public static partial class AutoInjectExtension
{
/// <summary>
/// 自动注册标注的服务
/// </summary>
/// <param name = "services"></param>
/// <returns></returns>
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
{
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
// ...
return services;
}
}
#pragma warning restore
大致的代码如下:
context.RegisterSourceOutput(join, (ctx, nodes) =>
{
var nodes1 = GetAnnotatedNodesInject(nodes.Left.Item1, nodes.Left.Item2);
var nodes2 = GetGenericAnnotatedNodesInject(nodes.Right.Item1, nodes.Right.Item2);
GenSource(ctx, [.. nodes1, .. nodes2]);
});
private static void GenSource(SourceProductionContext context, IEnumerable<AutoInjectDefine> injectDefines)
{
// 生成代码
StringBuilder classes = new();
injectDefines.Distinct().ToList().ForEach(define =>
{
if (define.ImplType != define.BaseType)
{
classes.AppendLine($@"services.{define.LifeTime}<{define.BaseType}, {define.ImplType}>();");
}
else
{
classes.AppendLine($@"services.{define.LifeTime}<{define.ImplType}>();");
}
});
string rawNamespace = string.Empty;
//_namespaces.Distinct().ToList().ForEach(ns => rawNamespace += $"using {ns};\r\n");
var envSource = Template.Replace("$services", classes.ToString());
envSource = envSource.Replace("$namespaces", rawNamespace);
// format:
envSource = FormatContent(envSource);
context.AddSource($"Biwen.AutoClassGenInject.g.cs", SourceText.From(envSource, Encoding.UTF8));
}
/// <summary>
/// 格式化代码
/// </summary>
/// <param name="csCode"></param>
/// <returns></returns>
private static string FormatContent(string csCode)
{
var tree = CSharpSyntaxTree.ParseText(csCode);
var root = tree.GetRoot().NormalizeWhitespace();
var ret = root.ToFullString();
return ret;
}
private const string Template = """
// <auto-generated />
// issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
// 如果你在使用中遇到问题,请第一时间issue,谢谢!
// This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
#pragma warning disable
$namespaces
public static partial class AutoInjectExtension
{
/// <summary>
/// 自动注册标注的服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
{
$services
return services;
}
}
#pragma warning restore
""";
最终工具会自动为你生成以下代码:
// <auto-generated />
// issue:https://github.com/vipwan/Biwen.AutoClassGen/issues
// 如果你在使用中遇到问题,请第一时间issue,谢谢!
// This file is generated by Biwen.AutoClassGen.AutoInjectSourceGenerator
#pragma warning disable
public static partial class AutoInjectExtension
{
/// <summary>
/// 自动注册标注的服务
/// </summary>
/// <param name = "services"></param>
/// <returns></returns>
public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
{
services.AddScoped<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
services.AddTransient<Biwen.AutoClassGen.TestConsole.Services.TestService2>();
//...
return services;
}
}
#pragma warning restore
以上代码就完成了整个源生成步骤,最后你可以使用我发布的nuget包体验:
dotnet add package Biwen.AutoClassGen
源代码我发布到了GitHub,欢迎star! https://github.com/vipwan/Biwen.AutoClassGen
使用.NET源生成器(SG)实现一个自动注入的生成器的更多相关文章
- CommunityToolkit.Mvvm8.1 viewmodel源生成器写法(3)
本系列文章导航 https://www.cnblogs.com/aierong/p/17300066.html https://github.com/aierong/WpfDemo (自我Demo地址 ...
- delphi 一个自动控制机的硅控板检测程序,用多线程和API,没有用控件,少做改动就能用 用485开发
一个自动控制机的硅控板检测程序,用多线程和API,没有用控件,少做改动就能用Unit CommThread; Interface Uses Windows, Classes, SysUtils, G ...
- C++11实现一个自动注册的工厂
实现动机 工厂方法是最简单地创建派生类对象的方法,也是很常用的,工厂方法内部使用switch-case根据不同的key去创建不同的派生类对象,下面是一个伪代码. Message* create(int ...
- [CareerCup] 12.6 Test an ATM 测试一个自动取款机
12.6 How would you test an ATM in a distributed banking system? 这道题问我们如何来测试一个自动取款机,我们首先要询问下列问题: - 谁来 ...
- 打造一个自动检测页面是否存在XSS的小插件
前言: 还记得刚玩Web安全时,就想着要是能有一个自动挖掘XSS漏洞的软件就好了.然后我发现了Safe3.JSky.AWVS.Netsparker等等,但是误报太多,而且特别占内存.后来发现了fidd ...
- 做一个自动修改本机IP和mac的bat文件
原文:做一个自动修改本机IP和mac的bat文件 1.ip bat修改理论探讨 前两天我突然萌生了一个念头:能不能做一个小程序来实现自动配置或修改IP和mac,达到一键搞定的目的,这样尤其适合那些带着 ...
- 初步学习nodejs,业余用node写个一个自动创建目录和文件的小脚本,希望对需要的人有所帮助
初步学习nodejs,业余用node写个一个自动创建目录和文件的小脚本,希望对需要的人有所帮助,如果有bug或者更好的优化方案,也请批评与指正,谢谢,代码如下: var fs = require('f ...
- Shell 命令行,写一个自动整理 ~/Downloads/ 文件夹下文件的脚本
Shell 命令行,写一个自动整理 ~/Downloads/ 文件夹下文件的脚本 在 mac 或者 linux 系统中,我们的浏览器或者其他下载软件下载的文件全部都下载再 ~/Downloads/ 文 ...
- Python写一个自动点餐程序
Python写一个自动点餐程序 为什么要写这个 公司现在用meican作为点餐渠道,每天规定的时间是早7:00-9:40点餐,有时候我经常容易忘记,或者是在地铁/公交上没办法点餐,所以总是没饭吃,只有 ...
- 自己实现简单的AOP(五)使Demo适应webApi、亦可完成属性自动注入
在前文的Demo中,webApi的Controller是不能自动注入的,原因是 IHttpController 和 IController 是通过两个不同的途径进行激活的. IHttpControll ...
随机推荐
- KingbaseES V8R6 最老事务阻止vacuum freeze
前言 最近生产环境发生几次由于长事务导致表.库年龄没法回收的情况.我们要规避这种情况的发生,不要等发生了再去强制中断会话连接. 当数据库中存在最老事务版本xmin,那么早于他的快照可以被标记为froz ...
- KingbaseES V8R6 空闲事务会话超时自动终止机制
背景 如果会话在事务中停留的时间过长,则允许自动终止空闲会话.可以由配置参数idle_in_transaction_session_timeout 事务处于空闲状态的时长,它有助于防止被遗忘的交易事务 ...
- 实际项目中如何使用Git做分支管理
前言 Git是一种强大的分布式版本控制系统,在实际项目开发中使用Git进行分支管理是非常常见的做法,因为它可以帮助团队高效的协作和管理项目的不同版本,今天我们来讲讲在实际项目中最常用的Git分支管理策 ...
- #树状数组,dp#SGU 521 North-East
题目 在平面上有 \(n\) 个点,现在有一个人要从某个点出发, 每次只能到达横纵坐标都超过原坐标的点,也就是 \(x_j<x_i,y_j<y_i\) 如果他要经过最多的点,那么哪些点是可 ...
- #树状数组,dp#洛谷 3506 [POI2010]MOT-Monotonicity 2
题目 给出\(N\)个正整数\(a[1..N]\),再给出\(K\)个关系符号(>.<或=)\(s[1..k]\). 选出一个长度为\(L\)的子序列(不要求连续),要求这个子序列的第\( ...
- java中的类型擦除type erasure
目录 简介 举个例子 原因 解决办法 总结 简介 泛型是java从JDK 5开始引入的新特性,泛型的引入可以让我们在代码编译的时候就强制检查传入的类型,从而提升了程序的健壮度. 泛型可以用在类和接口上 ...
- 深入理解java的泛型
目录 简介 泛型和协变 泛型在使用中会遇到的问题 类型擦除要注意的事项 总结 简介 泛型是JDK 5引入的概念,泛型的引入主要是为了保证java中类型的安全性,有点像C++中的模板. 但是Java为了 ...
- C语言简易万年历带注释
同学问的课后作业,顺便加了写注释. #include<stdio.h> /* * 注意 每周的第一天是星期天 */ int main() { int day_per_mo[12] = { ...
- C# 发布你的程序包到Nuget
1.新建一个.NET Standard 的类库项目 2.选择项目属性,在 package 栏目下填写我们的nuget包信息 3.选择我们的项目,点击"Pack" 打包 主要注意的是 ...
- 开发指导—利用CSS动画实现HarmonyOS动效(一)
注:本文内容分享转载自HarmonyOS Developer官网文档 一. CSS语法参考 CSS是描述HML页面结构的样式语言.所有组件均存在系统默认样式,也可在页面CSS样式文件中对组件.页面自 ...