基于AutoMapper添加正向映射特性

warning: 这篇文章距离上次修改已过516天,其中的内容可能已经有所变动。

在使用AutoMapper这个类库时,发现有基于特性的映射方式(对于大多数DTO只是Domain的子集的情况来说,写映射配置实在是很繁琐的一件事情),官方在文档上也说了一句Mapping code is boring,这也是AutoMapper面世的主要原因之一。

AutoMap

AutoMap是官方提供的特性,可以创建一个反向映射。

什么是反向映射,就是从目标类映射到当前类的情况,看一个例子:

/// <summary>
///     注册输入参数
/// </summary>
[AutoMap(typeof(User))]
public class RegistDto
{
    /// <summary>
    ///     用户名
    /// </summary>
    /// <example>superAdmin</example>
    [Required(ErrorMessage = "用户名不能为空")]
    [MinLength(3, ErrorMessage = "用户名不能少于3位字符")]
    public string? Email { get; set; }

    /// <summary>
    ///     公司名
    /// </summary>
    /// <example>superAdmin</example>
    [Required(ErrorMessage = "公司不能为空")]
    [MinLength(3, ErrorMessage = "公司不能少于3位字符")]
    public string? CompanyName { get; set; }

    /// <summary>
    ///     密码
    /// </summary>
    /// <example>123456</example>
    [Required(ErrorMessage = "密码不能为空")]
    [MinLength(5, ErrorMessage = "密码不能少于5位字符")]
    public string? Password { get; set; }
}

上述例子就创建了一个从UserRegistDto的映射关系,因为特性在RegistDto上,所以对它来说这是一种反向映射关系,目标是自己。

ReverseMap

那么如果我们要将RegistDto映射到User怎么办呢?

答案就是:ReverseMap,只需要在特性上标注添加反向映射即可:

[AutoMap(typeof(User), ReverseMap = true)]

这样就会创建两个映射:

  1. User -> RegistDto
  2. RegistDto -> User

但这里要注意一点:ReverseMap仅创建简单映射,什么意思呢,那就是所有在属性、字段上的特性它都不会处理,仅仅是调用了CreateMap而已。

缺点:

  1. 会创建双向映射(有时候我们只需要一个方向而已)。
  2. 只创建简单映射,不会处理其余特性。

为什么不在User上加一个AutoMap呢?

没错,可以!

但个人觉得这并不好,因为Domain层就会引入:

  1. AutoMapper
  2. DTO

本身DTO又需要引入Domain,这里就形成了循环依赖。再者更重要的一个原因, Domain不需要去关心这个事,这是映射要负责的内容,不应该污染到Domain

另一个原因:统一管理,特性是个好东西,但也不能无限制的滥用。一旦使用了,映射关系就会变得比较隐晦,因此最好统一去标注。这里我们选择放在了DTO身上,这样我们就知道这个DTO会变成谁,以及是由谁转换来的。

创建一个兼容AutoMapper原生特性的正向映射特性

我们去看AutoMap这个特性的实现,会发现一个很有意思的现象:

  1. 特性只提供开关控制及配置,不负责属性、字段特性实现细节。
  2. 提供IMappingExpression接口(其实具体应该是IMappingExpressionBase<TSource, TDestination, out TMappingExpression>),对外提供特性对应的能力。
  3. 暴露ApplyConfiguration(IMappingExpression mappingExpression)以便使特性生效。

那么从以上总结来看,我们完全可以借用这个机制很轻松的来做一个正向映射:

  1. AutoMap提供一样的配置特性(除ReverseMap)
  2. 暴露ApplyConfiguration方法
  3. Profile中扫描注册

创建AutoMapToAttribute


/// <summary>
///     指定映射目标(可以创建多个注解)
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = true)]
public class AutoMapToAttribute : Attribute
{
    public AutoMapToAttribute(Type targetType)
    {
        TargetType = targetType;
    }

    /// <summary>
    ///     目标类型
    /// </summary>
    public Type TargetType { get; }

    /// <summary>
    ///     If set to true, construct the destination object using the service locator.
    /// </summary>
    public bool ConstructUsingServiceLocator { get; set; }

    /// <summary>
    ///     限制最深的深度
    /// </summary>
    public int MaxDepth { get; set; }

    /// <summary>
    ///     If set to true, preserve object identity. Useful for circular references.
    /// </summary>
    public bool PreserveReferences { get; set; }

    /// <summary>
    ///     If set to true, disable constructor validation.
    /// </summary>
    public bool DisableCtorValidation { get; set; }

    /// <summary>
    ///     If set to true, include this configuration in all derived types' maps.
    /// </summary>
    public bool IncludeAllDerived { get; set; }

    /// <summary>
    ///     使用converter进行映射,忽略作用在成员属性上的注解
    /// </summary>
    public Type? TypeConverter { get; set; }

    public void ApplyConfiguration(IMappingExpression mappingExpression)
    {
        if (ConstructUsingServiceLocator) mappingExpression.ConstructUsingServiceLocator();

        if (MaxDepth > 0) mappingExpression.MaxDepth(MaxDepth);

        if (PreserveReferences) mappingExpression.PreserveReferences();

        if (DisableCtorValidation) mappingExpression.DisableCtorValidation();

        if (IncludeAllDerived) mappingExpression.IncludeAllDerived();

        if (TypeConverter != null) mappingExpression.ConvertUsing(TypeConverter);
    }
}

Profile中扫描注册

首先扫描出来所有添加了AutoMapToAttribute的类型:

    /// <summary>
    ///     是否可以被实例化出来
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static bool CanInstanced(this Type type)
    {
        return type.IsClass && !type.IsAbstract;
    }

    internal static IEnumerable<Type> GetMapToTypes(this Assembly[] assemblies)
    {
        var allTypes = assemblies.SelectMany(t => t.GetTypes());
        return allTypes.Where(t => t.CanInstanced())
            .Where(t => t.HasAttribute<AutoMapToAttribute>());
    }

扫描出来以后就将类上面所有的AutoMapTo特性取出来,逐个处理:

    /// <summary>
    ///     创建所有基于 MapTo 的映射
    /// </summary>
    /// <param name="assemblies"></param>
    private void AddMapToMaps(Assembly[] assemblies)
    {
        var types = assemblies.GetMapToTypes();
        foreach (var type in types)
        {
            var maps = type.GetAttributes<AutoMapToAttribute>();
            foreach (var map in maps) AddMapToMap(type, map);
        }
    }

手动创建Map并处理所有注解:

    /// <summary>
    ///     基于 MapTo 创建单个映射
    /// </summary>
    /// <param name="type"></param>
    /// <param name="map"></param>
    private void AddMapToMap(Type type, AutoMapToAttribute map)
    {
        var expression = CreateMap(type, map.TargetType);

        // 处理所有属性上的注解
        var configurableMembers = type.GetMembers(BindingFlags.Public | BindingFlags.Instance);
        foreach (var memberInfo in configurableMembers)
        foreach (var provider in memberInfo.GetCustomAttributes().OfType<IMemberConfigurationProvider>())
            expression.ForMember(memberInfo.Name, cfg => provider.ApplyConfiguration(cfg));

        // 处理 MapTo本身的委托
        map.ApplyConfiguration(expression);
    }

这样就完成了(在Profile里处理的,Profile会被AutoMapper自动扫描处理)

使用

/// <summary>
///     注册输入参数
/// </summary>
[AutoMapTo(typeof(User))]
public class RegistDto
{
    /// <summary>
    ///     用户名
    /// </summary>
    /// <example>superAdmin</example>
    [Required(ErrorMessage = "用户名不能为空")]
    [MinLength(3, ErrorMessage = "用户名不能少于3位字符")]
    public string? Email { get; set; }

    /// <summary>
    ///     公司名
    /// </summary>
    /// <example>superAdmin</example>
    [Required(ErrorMessage = "公司不能为空")]
    [MinLength(3, ErrorMessage = "公司不能少于3位字符")]
    public string? CompanyName { get; set; }

    /// <summary>
    ///     密码
    /// </summary>
    /// <example>123456</example>
    [Required(ErrorMessage = "密码不能为空")]
    [MinLength(5, ErrorMessage = "密码不能少于5位字符")]
    public string? Password { get; set; }
}

是不是和原生的体验一样丝滑呢~

AutoMapper这里处理特性的方式很值得学习,将创建映射的细节都抽象到接口中了,特性只是提供一个便利的口子而已,最终还是需要将接口的实例传进来进行处理,这样就和具体类型解耦了,特性完全不知道自己会被打在谁身上,ApplyConfiguration的时机也由框架来控制。
none
最后修改于:2022年12月20日 22:48

评论已关闭