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


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

    services.AddScoped();
    services.AddTransient();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped();
    services.AddScoped();
    services.AddSingleton();
    services.AddScoped();
    

    对于上面的代码如果代码量很大 而且随着项目的迭代可能会堆积更多的代码,对于很多程序员来说第一想到的可能是透过反射批量注入,当然这也是最简单最直接的方式,今天我们使用源生成器的方式实现这个功能, 使用源生成器的方式好处还是有的 比如AOT需求,极致性能要求

    实现这个功能的具体步骤:

    定义Attribute-标注Attribute-遍历代码中标注Attribute的metadata集合-生成源代码

    首先我们定义一个Attribute用于标注需要注入的类

    namespace Biwen.AutoClassGen.Attributes
    {
        using System;
        /// 
        /// 服务生命周期
        /// 
        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; }
            /// 
            /// 
            /// 
            /// NULL表示服务自身
            /// 服务生命周期
            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 : AutoInjectAttribute
        {
            public AutoInjectAttribute(ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) : base(typeof(T), serviceLifetime)
            {
            }
        }
    #endif
    }
    

    通过上面定义的Attribute我们就可以给我们的服务打上标记了

    [AutoInject]
    [AutoInject(ServiceLifetime.Singleton)]
    [AutoInject(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";
    
    /// 
    /// 泛型AutoInjectAttribute
    /// 
    private const string GenericAutoInjectAttributeName = "Biwen.AutoClassGen.Attributes.AutoInjectAttribute`1";
    
    /// 
    /// 非泛型AutoInjectAttribute
    /// 
    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)> compilationAndTypesInject =
    	context.CompilationProvider.Combine(nodesAutoInject);
    
    #endregion
    
    #region 泛型
    
    var nodesAutoInjectG = context.SyntaxProvider.ForAttributeWithMetadataName(
    GenericAutoInjectAttributeName,
    (context, attributeSyntax) => true,
    (syntaxContext, _) => syntaxContext.TargetNode).Collect();
    
    IncrementalValueProvider<(Compilation, ImmutableArray)> 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 GetGenericAnnotatedNodesInject(Compilation compilation, ImmutableArray nodes)
    {
    	if (nodes.Length == 0) return [];
    	// 注册的服务
    	List autoInjects = [];
    	List namespaces = [];
    
    	foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast())
    	{
    		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 = @"(?<=<)(?\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().Single().Name.ToString();
    				var symbols = compilation.GetSymbolsWithName(implTypeName);
    				foreach (ITypeSymbol symbol in symbols.Cast())
    				{
    					implTypeName = symbol.ToDisplayString();
    					break;
    				}
    
    				var baseSymbols = compilation.GetSymbolsWithName(baseTypeName);
    				foreach (ITypeSymbol baseSymbol in baseSymbols.Cast())
    				{
    					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())
    					{
    						var fullNameSpace = symbol.ContainingNamespace.ToDisplayString();
    						// 命名空间
    						if (!namespaces.Contains(fullNameSpace))
    						{
    							namespaces.Add(fullNameSpace);
    						}
    					}
    				}
    			}
    		}
    	}
    
    	return autoInjects;
    }
    
    

    解析所有标注非泛型Attribute的metadata集合

    
    private static List GetAnnotatedNodesInject(Compilation compilation, ImmutableArray nodes)
    {
    	if (nodes.Length == 0) return [];
    	// 注册的服务
    	List autoInjects = [];
    	List namespaces = [];
    
    	foreach (ClassDeclarationSyntax node in nodes.AsEnumerable().Cast())
    	{
    		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().Single().Name.ToString();
    				var symbols = compilation.GetSymbolsWithName(implTypeName);
    				foreach (ITypeSymbol symbol in symbols.Cast())
    				{
    					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())
    				{
    					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())
    					{
    						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
    {
        /// 
        /// 自动注册标注的服务
        /// 
        /// 
        /// 
        public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
        {
            services.AddScoped();
            services.AddTransient();
           // ...
            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 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));
    }
    /// 
    /// 格式化代码
    /// 
    /// 
    /// 
    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 = """
    	// 
    	// 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
    	{
    		/// 
    		/// 自动注册标注的服务
    		/// 
    		/// 
    		/// 
    		public static  Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
    		{
    			$services
    			return services;
    		}
    	}
    	
    	#pragma warning restore
    	""";
    
    

    最终工具会自动为你生成以下代码:

    // 
    // 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
    {
        /// 
        /// 自动注册标注的服务
        /// 
        /// 
        /// 
        public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddAutoInject(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)
        {
            services.AddScoped();
            services.AddTransient();
    		//...
            return services;
        }
    }
    #pragma warning restore
    
    

    以上代码就完成了整个源生成步骤,最后你可以使用我发布的nuget包体验:

    dotnet add package Biwen.AutoClassGen
    

    源代码我发布到了GitHub,欢迎star! https://github.com/vipwan/Biwen.AutoClassGen

  • 相关阅读:
    数据仓库与数据挖掘的第一章课后习题
    操作视频的开始与暂停
    突破开源天花板!最强文本转语音工具ChatTTS:对话式高可控的语音合成模型
    STP学习的第一篇
    C#面:switch 表达式可以用什么类型?能用 string 类型吗?
    软件测试不是所有人都适合的
    Linux网络编程(高并发服务器)
    Linux地址映射那些事,堪称初学者的噩梦!
    2023快递高质量发展高峰论坛暨2023上海国际快递物流产业博览会
    ES6 - 扩展运算符与Object.assign对象拷贝与合并
  • 原文地址:https://www.cnblogs.com/vipwan/p/18175230