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“,记住我!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值