问题
在业务开发中,我们常常需要将一个对象映射成另一个对象。例如将领域实体(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“,记住我!