使用.NET源生成器(SG)实现一个自动注入的生成器

net,sg · 浏览次数 : 115

小编点评

**代码解析** 该代码提供两个方法 `AddAutoInject` 用于自动注册标注服务: 1. **`AddScoped` 方法** 用于注册标注服务的 Scoped 类型。 2. **`AddTransient` 方法** 用于注册标注服务的 Transient 类型。 **代码生成步骤** 1. 定义一个 `AutoInjectDefine` 列表,其中包含标注服务的信息。 2. 使用 `ForEach` 方法遍历 `AutoInjectDefine` 中的每个定义,并创建相应的代码。 3. 将生成的代码添加到 `services` 中。 4. 返回 `services` 对象,表示服务注册成功。 **代码格式化** 代码使用 C# 语法进行格式化,其中包含以下元素: * `#pragma warning` 用于屏蔽编译器警告。 * `$namespace` 用于替换命名空间。 * `$template` 用于包含代码的模板。 * `C# 语法树` 用于构建代码树。 **最终代码** 最终代码包含以下部分: * 注册 Scoped 类型:`services.AddScoped();` * 注册 Transient 类型:`services.AddTransient();` **使用方法** 1. 创建一个 `IServiceCollection` 对象。 2. 调用 `AddAutoInject` 方法添加标注服务。 3. 运行应用程序。 **注意** 代码中没有提供 `namespaces` 用于命名空间的定义,因此可能需要手动添加命名空间。

正文

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)实现一个自动注入的生成器相似的内容:

使用.NET源生成器(SG)实现一个自动注入的生成器

DI依赖注入对我们后端程序员来说肯定是基础中的基础了,我们经常会使用下面的代码注入相关的service services.AddScoped(); services.AddTransient<

[转帖]Linux内核参数net.ipv4.ip_local_port_range对服务器连接数影响的正确解释

首先明确一下该参数的意义:net.ipv4.ip_local_port_range表示本机作为客户端对外发起tcp/udp连接时所能使用的临时端口范围。 对于TCP连接,本机所能发起的tcp连接数受四元组(源地址*源端口*目标地址*目标端口)限制。 而对于UDP连接,本机所能发起的udp连接数则受二

使用.NET查询日出日落时间

在WPF中,通过资源文件实现主题切换是个常见的功能,有不少文章介绍了如何实现手动切换主题。那如何实现自动切换主题呢?通常有两种机制:一是跟随系统明暗主题切换,二是像手机操作系统那样根据日出日落时间自动切换。本文将以终为始,采用倒推法一步步介绍如何使用.NET免费获取日出日落时间。 获取日出日落时间

Python使用.NET开发的类库来提高你的程序执行效率

Python由于本身的特性原因,执行程序期间可能效率并不是很理想。在某些需要自己提高一些代码的执行效率的时候,可以考虑使用C#、C++、Rust等语言开发的库来提高python本身的执行效率。接下来,我演示一种使用.NET平台开发的类库,来演示一下Python访问.NET类库的操作实现。类库演示包括

.NET使用原生方法实现文件压缩和解压

前言 在.NET中实现文件或文件目录压缩和解压可以通过多种方式来完成,包括使用原生方法(System.IO.Compression命名空间中的类)和第三方库(如:SharpZipLib、SharpCompress、K4os.Compression.LZ4等)。本文我们主要讲的是如何使用.NET原生方

【译】使用 .NET Aspire 和 Visual Studio 开发云原生应用

我们很高兴地宣布 .NET Aspire 发布,它扩展了 Visual Studio 在云原生应用程序开发方面的能力。.NET Aspire 提供了一个框架和工具,以一种独特的方式,使分布式 .NET 应用程序更容易构建、部署和管理。这种集成旨在使云原生应用程序的开发更加简单和高效。

Gin 框架是怎么使用 net http 包的(gin.go)

Gin 框架是基于 Go 语言的标准库 net/http 构建的,它使用 net/http 提供的基础功能来构建自己的高性能 Web 应用框架。 具体来说,Gin 使用 net/http 的以下方面: 1,HandlerFunc: Gin 使用 net/http 的 HandlerFunc 类型,这

[转帖]windows使用net user add用户并加入管理员,从而支持rdp远程登陆访问

C:\phpstudy_pro\WWW> net user test2 /add 命令成功完成。 C:\phpstudy_pro\WWW> net user test2 Huawei@123 命令成功完成。 C:\phpstudy_pro\WWW> net localgroup administra

.NET快速实现网页数据抓取

前言 今天我们来讲讲如何使用.NET开源(MIT License)的轻量、灵活、高性能、跨平台的分布式网络爬虫框架DotnetSpider来快速实现网页数据抓取功能。 注意:为了自身安全请在国家法律允许范围内开发网页爬虫功能。 网页数据抓取需求 本文我们以抓取博客园10天推荐排行榜第一页的文章标题、

Windows 环境下载、安装、使用(.Net 5.0) Redis 数据库及常见问题的解决

本文简单介绍了 Redis 的下载、安装,以及基于 ServiceStack.Redis.dll 的使用,希望对你有帮助。