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原理讲解

究竟是什么可以比反射还快实现动态调用?| Source Generators版

如何定位Source Generators性能问题

Hello Blazor:Source Generators生成导航菜单

.NET Core开发实战(定义API的最佳实践)Source Generators版

Source Generators(源代码生成器)的调试器支持 | Visual Studio 2019(16.10)新功能试用...