使用Roslyn的源生成器生成DTO

前言

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

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

功能演示

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

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

csharp 复制代码
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并标注特性:

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

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

csharp 复制代码
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扩展方法:

csharp 复制代码
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,
		};
	}
}

实现代码

csharp 复制代码
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};\r\n");

			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}\r\n{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包体验:

xml 复制代码
<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

相关推荐
AzulX20 天前
Natasha v9.0 为 .NET 开发者提供 [热执行] 方案.
c#·.net·roslyn·natasha
Flamesky20 天前
dotnet core微服务框架Jimu ~ 基础网关
微服务·c#·service·dotnet·csharp·micro·micro service
万雅虎3 个月前
使用 `Roslyn` 分析器和修复器 对异步方法规范化返回Async结尾
netcore·roslyn·csharp·sg
万雅虎4 个月前
使用.NET源生成器(SG)生成项目的版本号信息
roslyn·csharp·sg
@背包4 个月前
C# 版本八大排序算法|.Net 架构
算法·排序算法·csharp
小康师兄5 个月前
IoTDB 入门教程 实战篇④——C#示例(开源)
开源·c#·时序数据库·iotdb·csharp
万雅虎5 个月前
C#中使用 record 的好处 因为好用所以推荐~
csharp
万雅虎5 个月前
如何在.NET Framework,或NET8以前的项目中使用C# 12的新特性
csharp
万雅虎5 个月前
C# 使用模式匹配的好处,因为好用所以推荐~
csharp