- A+
所属分类:.NET技术
前言
源生成器的好处很多, 通过在编译时生成代码,可以减少运行时的反射和动态代码生成,从而提高应用程序的性能, 有时候需要对程序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