使用Roslyn的源生成器生成DTO

前言

源生成器的好处很多, 通过在编译时生成代码,可以减少运行时的反射和动态代码生成,从而提高应用程序的性能, 有时候需要对程序AOT以及裁剪编译的dll也是需要用SG来处理的。

我们开发程序应该都绕不过Mapper对象映射,用的比较多的库可能就是AutoMapper,Maspter之内的三方库吧;这些库很强大但是因为内部实现存在反射,因此开发的程序就没办法AOT了,因此如果程序不是很复杂但是又有很特殊的需求,建议使用SG来实现Mapper

功能演示

这里我演示下自己开发的AutoDto生成DTO功能:

比如我们有一个User的类,需要生成UserDto

public class User { 	public string Id { get; set; } = null!; 	public string FirstName { get; set; } = null!; 	public string LastName { get; set; } = null!; 	public int? Age { get; set; } 	public string? FullName => $"{FirstName} {LastName}"; } 

定义UserDto并标注特性:

[AutoDto<User>(nameof(User.Id))]//这里我们假设排除Id属性 public partial record UserDto; 

就这样,源生成器将自动为我们生成对应的Dto:

partial record class UserDto { 	/// <inheritdoc cref = "User.FirstName"/> 	public string FirstName { get; set; } 	/// <inheritdoc cref = "User.LastName"/> 	public string LastName { get; set; } 	/// <inheritdoc cref = "User.Age"/> 	public int? Age { get; set; } } 

并同时为我们生成一个简单的Mapper扩展方法:

public static partial class UserToUserDtoExtentions { 	/// <summary> 	/// mapper to UserDto 	/// </summary> 	/// <returns></returns> 	public static UserDto MapperToUserDto(this User model) 	{ 		return new UserDto() 		{ 			FirstName = model.FirstName, 			LastName = model.LastName, 			Age = model.Age, 			FullName = model.FullName, 		}; 	} } 

实现代码

static void GENDTO(Compilation compilation, ImmutableArray<SyntaxNode> nodes, SourceProductionContext context) { 	if (nodes.Length == 0) return; 	StringBuilder envStringBuilder = new(); 	envStringBuilder.AppendLine("// <auto-generated />"); 	envStringBuilder.AppendLine("using System;"); 	envStringBuilder.AppendLine("using System.Collections.Generic;"); 	envStringBuilder.AppendLine("using System.Text;"); 	envStringBuilder.AppendLine("using System.Threading.Tasks;"); 	envStringBuilder.AppendLine("#pragma warning disable");  	foreach (var nodeSyntax in nodes.AsEnumerable()) 	{ 		//Cast<ClassDeclarationSyntax>() 		//Cast<RecordDeclarationSyntax>() 		if (nodeSyntax is not TypeDeclarationSyntax node) 		{ 			continue; 		} 		//如果是Record类 		var isRecord = nodeSyntax is RecordDeclarationSyntax; 		//如果不含partial关键字,则不生成 		if (!node.Modifiers.Any(x => x.IsKind(SyntaxKind.PartialKeyword))) 		{ 			continue; 		}  		AttributeSyntax? attributeSyntax = null; 		foreach (var attr in node.AttributeLists.AsEnumerable()) 		{ 			var attrName = attr.Attributes.FirstOrDefault()?.Name.ToString(); 			if (attrName?.IndexOf(AttributeValueMetadataNameDto, System.StringComparison.Ordinal) == 0) 			{ 				attributeSyntax = attr.Attributes.First(x => x.Name.ToString().IndexOf(AttributeValueMetadataNameDto, System.StringComparison.Ordinal) == 0); 				break; 			} 		} 		if (attributeSyntax == null) 		{ 			continue; 		} 		//转译的Entity类名 		var entityName = string.Empty; 		string pattern = @"(?<=<)(?<type>w+)(?=>)"; 		var match = Regex.Match(attributeSyntax.ToString(), pattern); 		if (match.Success) 		{ 			entityName = match.Groups["type"].Value.Split(['.']).Last(); 		} 		else 		{ 			continue; 		}  		var sb = new StringBuilder(); 		sb.AppendLine(); 		sb.AppendLine($"//generate {entityName}-{node.Identifier.ValueText}"); 		sb.AppendLine(); 		sb.AppendLine("namespace $ni"); 		sb.AppendLine("{"); 		sb.AppendLine("$namespace"); 		sb.AppendLine("$classes"); 		sb.AppendLine("}"); 		// sb.AppendLine("#pragma warning restore"); 		string classTemp = $"partial $isRecord $className  {{ $body }}"; 		classTemp = classTemp.Replace("$isRecord", isRecord ? "record class" : "class");  		{ 			// 排除的属性 			List<string> excapes = [];  			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.InvocationExpression)) 					{ 						var name = (expressionSyntax as InvocationExpressionSyntax)!.ArgumentList.DescendantNodes().First().ToString(); 						excapes.Add(name.Split(['.']).Last()); 					} 					else if (expressionSyntax.IsKind(SyntaxKind.StringLiteralExpression)) 					{ 						var name = (expressionSyntax as LiteralExpressionSyntax)!.Token.ValueText; 						excapes.Add(name); 					} 				} 			} 			var className = node.Identifier.ValueText; 			var rootNamespace = string.Empty; 			//获取文件范围的命名空间 			var filescopeNamespace = node.AncestorsAndSelf().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault(); 			if (filescopeNamespace != null) 			{ 				rootNamespace = filescopeNamespace.Name.ToString(); 			} 			else 			{ 				rootNamespace = node.AncestorsAndSelf().OfType<NamespaceDeclarationSyntax>().Single().Name.ToString(); 			} 			StringBuilder bodyBuilder = new(); 			List<string> namespaces = []; 			StringBuilder bodyInnerBuilder = new(); 			StringBuilder mapperBodyBuilder = new(); 			bodyInnerBuilder.AppendLine(); 			List<string> haveProps = []; 			// 生成属性 			void GenProperty(TypeSyntax @type) 			{ 				var symbols = compilation.GetSymbolsWithName(@type.ToString(), SymbolFilter.Type);  				foreach (ITypeSymbol symbol in symbols.Cast<ITypeSymbol>()) 				{ 					var fullNameSpace = symbol.ContainingNamespace.ToDisplayString(); 					// 命名空间 					if (!namespaces.Contains(fullNameSpace)) 					{ 						namespaces.Add(fullNameSpace); 					} 					symbol.GetMembers().OfType<IPropertySymbol>().ToList().ForEach(prop => 																				   { 																					   if (!excapes.Contains(prop.Name)) 																					   { 																						   // 如果存在同名属性,则不生成 																						   if (haveProps.Contains(prop.Name)) 																						   { 																							   return; 																						   }  																						   haveProps.Add(prop.Name);  																						   //如果是泛型属性,则不生成 																						   if (prop.ContainingType.TypeParameters.Any(x => x.Name == prop.Type.Name)) 																						   { 																							   return; 																						   }  																						   // prop: 																						   var raw = $"public {prop.Type.ToDisplayString()} {prop.Name} {{get;set;}}"; 																						   // body: 																						   bodyInnerBuilder.AppendLine($"/// <inheritdoc cref="{@type}.{prop.Name}" />"); 																						   bodyInnerBuilder.AppendLine($"{raw}");  																						   // mapper: 																						   // 只有public的属性才能赋值 																						   if (prop.GetMethod?.DeclaredAccessibility == Accessibility.Public) 																						   { 																							   mapperBodyBuilder.AppendLine($"{prop.Name} = model.{prop.Name},"); 																						   } 																					   } 																				   }); 				} 			}  			// 生成属性: 			var symbols = compilation.GetSymbolsWithName(entityName, SymbolFilter.Type); 			var symbol = symbols.Cast<ITypeSymbol>().FirstOrDefault(); 			//引用了其他库. 			if (symbol is null) 				continue; GenProperty(SyntaxFactory.ParseTypeName(symbol.MetadataName));  			// 生成父类的属性: 			INamedTypeSymbol? baseType = symbol.BaseType; 			while (baseType != null) 			{ 				GenProperty(SyntaxFactory.ParseTypeName(baseType.MetadataName)); 				baseType = baseType.BaseType; 			}  			var rawClass = classTemp.Replace("$className", className); 			rawClass = rawClass.Replace("$body", bodyInnerBuilder.ToString()); 			// append: 			bodyBuilder.AppendLine(rawClass);  			string rawNamespace = string.Empty; 			namespaces.ForEach(ns => rawNamespace += $"using {ns};rn");  			var source = sb.ToString(); 			source = source.Replace("$namespace", rawNamespace); 			source = source.Replace("$classes", bodyBuilder.ToString()); 			source = source.Replace("$ni", rootNamespace);  			// 生成Mapper 			var mapperSource = MapperTemplate.Replace("$namespace", namespaces.First()); 			mapperSource = mapperSource.Replace("$ns", rootNamespace); 			mapperSource = mapperSource.Replace("$baseclass", entityName); 			mapperSource = mapperSource.Replace("$dtoclass", className); 			mapperSource = mapperSource.Replace("$body", mapperBodyBuilder.ToString());  			// 合并 			source = $"{source}rn{mapperSource}"; 			envStringBuilder.AppendLine(source); 		} 	}  	envStringBuilder.AppendLine("#pragma warning restore"); 	var envSource = envStringBuilder.ToString(); 	// format: 	envSource = envSource.FormatContent(); 	context.AddSource($"Biwen.AutoClassGenDtoG.g.cs", SourceText.From(envSource, Encoding.UTF8)); }  const string MapperTemplate = $@" namespace $namespace {{     using $ns ;     public static partial class $baseclassTo$dtoclassExtentions     {{         /// <summary>         /// mapper to $dtoclass         /// </summary>         /// <returns></returns>         public static $dtoclass MapperTo$dtoclass(this $baseclass model)         {{             return new $dtoclass()             {{                 $body             }};         }}     }} }} "; 

最后

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

<ItemGroup>    <PackageReference Include="Biwen.AutoClassGen.Attributes" Version="1.3.6" />    <PackageReference Include="Biwen.AutoClassGen" Version="1.5.2" PrivateAssets="all" /> </ItemGroup> 

当然如果你对完整的实现感兴趣可以移步我的GitHub仓储,欢迎star https://github.com/vipwan/Biwen.AutoClassGen

发表评论

相关文章