Source Generators实现简版AutoMapper
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Source Generators实现简版AutoMapper相关的知识,希望对你有一定的参考价值。
问题
在业务开发中,我们常常需要将一个对象映射成另一个对象。例如将领域实体(UserEntity)映射成暴露给服务外部使用的数据传输对象(UserDto)。
而AutoMapper
则是目前主流的解决方案,实现类似如下代码:
var configuration = new MapperConfiguration(cfg =>
{
cfg.CreateMap<UserEntity, UserDto>();
});
var mapper = configuration.CreateMapper();
var userEntity = GetFromDB();
var userDto = mapper.Map<UserDto>(userEntity);
相对于使用AutoMapper
,我更倾向于显式映射,类似如下代码:
public UserDto MapToUserDto(UserEntity entity)
{
return new UserDto {
Id = entity.Id,
Name = entity.Name
};
}
var userEntity = GetFromDB();
var userDto = MapToUserDto(userEntity);
显式映射有以下一些好处:
不依赖第三方框架,性能有保障
设计时支持,例如"查找所有引用"
运行时支持,例如"断点调试"
但是缺点也很明显,手工编写显式映射是一项耗时并且枯燥的工作。
虽然可以使用工具(例如代码生成器)自动生成这些映射代码,但是今天我们介绍一种更方便的方式。
Source Generators
上次我们已经介绍过Source Generators,它可以在编译时创建并添加到编译中的代码,而无需像代码生成器那样显式生成大量冗余代码。
因此,我们这次尝试用Source Generators来自动生成显式映射代码。
实现代码如下:
[Generator]
public class AutoMapperGenerator : ISourceGenerator
{
private const string MappingAttributeText = @"
using System;
namespace AutoMapperGenerator
{
public class AutoMappingAttribute : Attribute
{
public AutoMappingAttribute(Type fromType,Type toType)
{
this.FromType = fromType;
this.ToType = toType;
}
public Type FromType { get; set; }
public Type ToType { get; set; }
}
}";
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
context.AddSource("AutoMappingAttribute", SourceText.From(MappingAttributeText, Encoding.UTF8));
var options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(MappingAttributeText, Encoding.UTF8), options));
var allNodes = compilation.SyntaxTrees.SelectMany(s => s.GetRoot().DescendantNodes());
var allAttributes = allNodes.Where((d) => d.IsKind(SyntaxKind.Attribute)).OfType<AttributeSyntax>();
var attributes = allAttributes.Where(d => d.Name.ToString() == "AutoMapping").ToList();
var allClasses = compilation.SyntaxTrees.
SelectMany(x => x.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>());
var sourceBuilder = new StringBuilder(@"
//<auto-generated>
namespace AutoMapperGenerator
{
public static class Mapper
{");
foreach (AttributeSyntax attr in attributes)
{
var fromTypeArgSyntax = attr.ArgumentList.Arguments.First();
var fromTypeArgSyntaxExpr = fromTypeArgSyntax.Expression.NormalizeWhitespace().ToFullString();
var toTypeArgSyntax = attr.ArgumentList.Arguments.ElementAt(1);
var toTypeArgSyntaxExpr = toTypeArgSyntax.Expression.NormalizeWhitespace().ToFullString();
var fromClassName = GetContentInParentheses(fromTypeArgSyntaxExpr);
var fromClassSyntax = allClasses.First(x => x.Identifier.ToString() == fromClassName);
var fromClassModel = compilation.GetSemanticModel(fromClassSyntax.SyntaxTree);
var fromClassNamedTypeSymbol = ModelExtensions.GetDeclaredSymbol(fromClassModel, fromClassSyntax);
var fromClassFullName = fromClassNamedTypeSymbol.OriginalDefinition.ToString();
var toClassName = GetContentInParentheses(toTypeArgSyntaxExpr);
var toClassSyntax = allClasses.First(x => x.Identifier.ToString() == toClassName);
var toClassModel = compilation.GetSemanticModel(toClassSyntax.SyntaxTree);
var toClassNamedTypeSymbol = ModelExtensions.GetDeclaredSymbol(toClassModel, toClassSyntax);
var toClassFullName = toClassNamedTypeSymbol.OriginalDefinition.ToString();
sourceBuilder.Append($@"
public static {toClassFullName} To{toClassName}(this {fromClassFullName} source)
{{
var target = new {toClassFullName}();");
var propertySyntaxes = toClassSyntax.SyntaxTree.GetRoot().DescendantNodes().OfType<PropertyDeclarationSyntax>();
foreach (var propertySyntaxe in propertySyntaxes)
{
var symbol = toClassModel.GetDeclaredSymbol(propertySyntaxe);
var propertyName = symbol.Name;
sourceBuilder.Append($@"
target.{propertyName} = source.{propertyName};");
}
sourceBuilder.Append(@"
return target;
}
");
}
sourceBuilder.Append(@"
}
}");
context.AddSource("Mapper", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
private string GetContentInParentheses(string value)
{
var match = Regex.Match(value, @"\\(([^)]*)\\)");
return match.Groups[1].Value;
}
}
我们定义了AutoMappingAttribute
,可以在任意类上声明此Attribute。
AutoMappingAttribute
包含FromType和ToType参数,Source Generators为FromType生成ToXXX
的扩展方法,遍历ToType对应类的所有属性并显示映射。
使用示例
示例代码如下:
[ApiController]
[Route("[controller]")]
[AutoMapping(typeof(UserEntity), typeof(UserDto))]
public class UserController : ControllerBase
{
[HttpGet]
public UserDto Get(int id)
{
var userEntity = GetFromDB(id);
var userDto = userEntity.ToUserDto();
return userDto;
}
}
在UserController
上声明了AutoMappingAttribute
,编译后可以看到,自动生成了ToUserDto
方法:
运行后测试,工作正常,成功!
结论
当然,目前的功能与真正的AutoMapper
还相差很远。
但是,如果你也希望在代码中使用显式映射,本文将是一个很好的起点。
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!
以上是关于Source Generators实现简版AutoMapper的主要内容,如果未能解决你的问题,请参考以下文章
究竟是什么可以比反射还快实现动态调用?| Source Generators版
Hello Blazor:Source Generators生成导航菜单
.NET Core开发实战(定义API的最佳实践)Source Generators版
Source Generators(源代码生成器)的调试器支持 | Visual Studio 2019(16.10)新功能试用...