基于AutoMapper添加正向映射特性
在使用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; }
}
上述例子就创建了一个从User到RegistDto的映射关系,因为特性在RegistDto上,所以对它来说这是一种反向映射关系,目标是自己。
ReverseMap
那么如果我们要将RegistDto映射到User怎么办呢?
答案就是:ReverseMap,只需要在特性上标注添加反向映射即可:
[AutoMap(typeof(User), ReverseMap = true)]
这样就会创建两个映射:
- User -> RegistDto
- RegistDto -> User
但这里要注意一点:ReverseMap仅创建简单映射,什么意思呢,那就是所有在属性、字段上的特性它都不会处理,仅仅是调用了CreateMap
而已。
缺点:
- 会创建双向映射(有时候我们只需要一个方向而已)。
- 只创建简单映射,不会处理其余特性。
为什么不在User上加一个AutoMap呢?
没错,可以!
但个人觉得这并不好,因为Domain层就会引入:
- AutoMapper
- DTO
本身DTO又需要引入Domain,这里就形成了循环依赖。再者更重要的一个原因, Domain不需要去关心这个事,这是映射要负责的内容,不应该污染到Domain。
另一个原因:统一管理,特性是个好东西,但也不能无限制的滥用。一旦使用了,映射关系就会变得比较隐晦,因此最好统一去标注。这里我们选择放在了DTO身上,这样我们就知道这个DTO会变成谁,以及是由谁转换来的。
创建一个兼容AutoMapper原生特性的正向映射特性
我们去看AutoMap这个特性的实现,会发现一个很有意思的现象:
- 特性只提供开关控制及配置,不负责属性、字段特性实现细节。
- 提供
IMappingExpression
接口(其实具体应该是IMappingExpressionBase<TSource, TDestination, out TMappingExpression>
),对外提供特性对应的能力。 - 暴露
ApplyConfiguration(IMappingExpression mappingExpression)
以便使特性生效。
那么从以上总结来看,我们完全可以借用这个机制很轻松的来做一个正向映射:
- 与AutoMap提供一样的配置特性(除ReverseMap)
- 暴露
ApplyConfiguration
方法 - 在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的时机也由框架来控制。
评论已关闭