本文基于Automapper 9.0.0
诊断Automapper的属性映射过程
var cfg = new MapperConfigurationExpression();
cfg.CreateMap<TypeB, TypeA>();
var configuration = new MapperConfiguration(cfg);
var executionPlan = configuration.BuildExecutionPlan(typeof(TypeB), typeof(TypeA));
最关键的一行代码var executionPlan = configuration.BuildExecutionPlan(typeof(TypeB), typeof(TypeA));
,此时查看executionPlan
可以看到它对应的表达式树解析,看到属性如何映射。这里有个小技巧,安装Visual Studio插件ReadableExpressions VS extension可以更清晰的看到表达式树解析。需要注意应该在正式发布时移除所有的调试代码。
参考http://docs.automapper.org/en/stable/Understanding-your-mapping.html
自动映射属性
Automapper会自动按照属性名去匹配映射关系,在默认映射中有以下一些特点:
- 不会区分属性名大小写,只要名称一样,大小写不一致也能够转换;
- 不会严格要求类型,字符串数值之间可以互相转换;
- 如果没匹配对应的属性,会继续寻找带
Get
前缀的方法名,也可以自动对目标类型进行PascalCase约定的分词查找,按照分词的顺序可以在源类型中深度查找内部对象。
类的成员对象向外映射
类型映射时想要映射的是Source类型的内部成员对象时,创建映射时需要加IncludeMembers
。
配置校验,测试映射关系
Automapper的映射是基于约定的,并不是强类型之间的手动映射,一个典型的应用场景是创建好映射之后,过了一段时间可能属性名变了,就会造成潜在的Bug,这时候就需要使用配置校验Configuration Validation
,如下:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, Destination>());
configuration.AssertConfigurationIsValid();
默认的校验规则是需要检查目标的每个属性都需要在来源中有映射,否则抛出异常。
有两种方式方式修改默认的校验规则:
- 使用
Ignore()
,var configuration = new MapperConfiguration(cfg => cfg.CreateMap<Source, Destination>().ForMember(dest => dest.SomeValuefff, opt => opt.Ignore()));
CreateMap
的时候修改校验规则,使用MemberList.Source
或MemberList.None
集合映射
如果要映射集合只需要映射集合对应的元素类型,支持的集合映射类型如下:
- IEnumerable
- IEnumerable
- ICollection
- ICollection
- IList
- IList
- List
- Arrays
如果要映射到一个已存在的集合,目标集合首先会被清空,具体详见AutoMapper.Collection
如果源的属性里有集合对象,并且属性为空,那么当它映射到目标时,会把属性映射为空集合,这一点符合C#关于集合的定义,数组、列表、集合、字典和IEnumerables永远不应该为null。在配置映射器时,可以通过将AllowNullCollections
属性设置为true
来更改此行为。
全局类型转换 Type Converters
在创建配置时使用ConvertUsing
,这个配置是全局的,只需要调用一次,它有以下三种重载方式:
void ConvertUsing(Func<TSource, TDestination> mappingFunction);
void ConvertUsing(ITypeConverter<TSource, TDestination> converter);
void ConvertUsing<TTypeConverter>() where TTypeConverter : ITypeConverter<TSource, TDestination>;
需要自定义源到目标之间的转换逻辑 Value Resolvers
当出现需要自定义转换逻辑时,主要使用以下接口:
public interface IValueResolver<in TSource, in TDestination, TDestMember>
{
TDestMember Resolve(TSource source, TDestination destination, TDestMember destMember, ResolutionContext context);
}
使用IValueResolver
接口有以下三种方式:
MapFrom<TValueResolver>
MapFrom(typeof(CustomValueResolver))
MapFrom(aValueResolverInstance)
除以上之外还可以继承IMemberValueResolver
接口,这个接口比上面那个多了来源属性的指定。
解析条件
如果我们指定了属性成员的映射,那在正式转换时可能会引发异常,这时候可以进行前置条件判断。
public class SourceClass
{
public string Value { get; set; }
}
public class TargetClass
{
public int ValueLength { get; set; }
}
// ...
var source = new SourceClass { Value = null };
var target = new TargetClass;
CreateMap<SourceClass, TargetClass>()
.ForMember(d => d.ValueLength, o => o.MapFrom(s => s.Value.Length))
.ForAllMembers(o => o.Condition((src, dest, value) => value != null));
.ForMember(d => d.ValueLength, o => o.MapFrom(s => s != null ? s.Value.Length : 0))
Value Converters
介于Type Converters
和Value Resolvers
之间的值转换方式:
Type converter = Func<TSource, TDestination, TDestination>
Value resolver = Func<TSource, TDestination, TDestinationMember>
Member value resolver = Func<TSource, TDestination, TSourceMember, TDestinationMember>
Value converter = Func<TSourceMember, TDestinationMember>
在成员级别配置该转换器
public class CurrencyFormatter : IValueConverter<decimal, string> {
public string Convert(decimal source)
=> source.ToString("c");
}
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.Amount, opt => opt.ConvertUsing(new CurrencyFormatter()));
cfg.CreateMap<OrderLineItem, OrderLineItemDto>()
.ForMember(d => d.Total, opt => opt.ConvertUsing(new CurrencyFormatter()));
});
如果属性的名称不能匹配,使用以下方式
public class CurrencyFormatter : IValueConverter<decimal, string> {
public string Convert(decimal source)
=> source.ToString("c");
}
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.Amount, opt => opt.ConvertUsing(new CurrencyFormatter(), src => src.OrderAmount));
cfg.CreateMap<OrderLineItem, OrderLineItemDto>()
.ForMember(d => d.Total, opt => opt.ConvertUsing(new CurrencyFormatter(), src => src.LITotal));
});
值转换
在值转换前判断是否应用了值转换器,可以在以下地方使用值转换器:
- Globally
- Profile
- Map
- Member
var configuration = new MapperConfiguration(cfg => {
cfg.ValueTransformers.Add<string>(val => val + "!!!");
});
var source = new Source { Value = "Hello" };
var dest = mapper.Map<Dest>(source);
dest.Value.ShouldBe("Hello!!!");
Null 转换
如果来源值为空,可以使用空转换器来给目标属性赋值,而不是使用源值
var config = new MapperConfiguration(cfg => cfg.CreateMap<Source, Dest>()
.ForMember(destination => destination.Value, opt => opt.NullSubstitute("Other Value")));
var source = new Source { Value = null };
var mapper = config.CreateMapper();
var dest = mapper.Map<Source, Dest>(source);
dest.Value.ShouldEqual("Other Value");
source.Value = "Not null";
dest = mapper.Map<Source, Dest>(source);
dest.Value.ShouldEqual("Not null");
映射前后逻辑处理
偶尔需要在映射时进行一些逻辑处理,可使用如下方式:
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Dest>()
.BeforeMap((src, dest) => src.Value = src.Value + 10)
.AfterMap((src, dest) => dest.Name = "John");
});
或者继承接口IMappingAction
:
public class NameMeJohnAction : IMappingAction<SomePersonObject, SomeOtherPersonObject>
{
public void Process(SomePersonObject source, SomeOtherPersonObject destination, ResolutionContext context)
{
destination.Name = "John";
}
}
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<SomePersonObject, SomeOtherPersonObject>()
.AfterMap<NameMeJohnAction>();
});
构造函数映射
如果目标类型没有默认的构造函数,Automap支持根据构造函数的形参名称来自动匹配源属性。
public class Source {
public int Value { get; set; }
}
public class SourceDto {
public SourceDto(int value) {
_value = value;
}
private int _value;
public int Value {
get { return _value; }
}
}
var configuration = new MapperConfiguration(cfg => cfg.CreateMap<Source, SourceDto>());
如果构造函数的参数名称无法自动匹配,可以使用ForCtorParam
来手动指定
public class Source {
public int Value { get; set; }
}
public class SourceDto {
public SourceDto(int valueParamSomeOtherName) {
_value = valueParamSomeOtherName;
}
private int _value;
public int Value {
get { return _value; }
}
}
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, SourceDto>()
.ForCtorParam("valueParamSomeOtherName", opt => opt.MapFrom(src => src.Value))
);
如果要禁用构造函数映射
var configuration = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
也可以选择要调用的目标构造函数
// don't map private constructors
var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = ci => !ci.IsPrivate);