前言
本文基于发稿时:
Automapper
的最新版本:10.1.1
。此扩展用于非ASP.NET Core程序。AutoMapper.Extensions.Microsoft.DependencyInjection
的最新版本:8.1.1。此扩展用于ASP.NET Core程序浏览的时候比较重要内容我在标题前加了“★”, 可以重点关注。
首先,配置映射关系
public class OrganizationProfile : Profile
{
public OrganizationProfile()
{
CreateMap<Foo, FooDto>();
}
}
如果是控制台应用,则:
var configuration = new MapperConfiguration(cfg => {
//cfg.CreateMap<Foo, Bar>();
cfg.AddProfile<OrganizationProfile>();//或者cfg.AddProfile(new OrganizationProfile());
});
var mapper=configuration.CreateMapper();//或者var mapper=new Mapper(configuration);
var dest=mapper.Map<OrderDto>(order);
如果ASP.NET Core应用,则(需安装AutoMapper.Extensions.Microsoft.DependencyInjection
):
//1. ConfigureServices里Add,入参类型为params
services.AddAutoMapper(typeof(OrganizationProfile));
//2. 然后在Controller里使用即可:
public XXXController(IMapper mapper)
{
_mapper = mapper;
var dest=mapper.Map<OrderDto>(order);
}
除了上述的手动添加profile,还可以自动扫描profile并添加:
//方法1
var configuration = new MapperConfiguration(cfg => cfg.AddMaps(myAssembly));
//方法2:通过程序集名
var configuration = new MapperConfiguration(cfg =>
cfg.AddMaps(new [] {
"Foo.UI",
"Foo.Core"
});
);
//方法3:通过typeof
var configuration = new MapperConfiguration(cfg =>
cfg.AddMaps(new [] {
typeof(HomeController),
typeof(Entity)
});
);
作用:驼峰命名与Pascal命名的兼容。
以下全局配置会映射property_name到PropertyName
var configuration = new MapperConfiguration(cfg => {
cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
});
或者针对某个profile进行配置(这种方式全局通用):
public class OrganizationProfile : Profile
{
public OrganizationProfile()
{
SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
DestinationMemberNamingConvention = new PascalCaseNamingConvention();
//Put your CreateMap... Etc.. here
}
}
var configuration = new MapperConfiguration(c =>
{
c.ReplaceMemberName("Ä", "A");
c.ReplaceMemberName("í", "i");
c.ReplaceMemberName("Airlina", "Airline");
});
进行以上配置之后,会自动将Äbc
映射到Abc
上,将íng
映射到ing
上,将AirlinaMark
映射到AirlineMark
上。
var configuration = new MapperConfiguration(cfg => {
cfg.RecognizePrefixes("frm");
//cfg.RecongizePostfixes("后缀");
cfg.CreateMap<Source, Dest>();
});
这样frmValue就可以map到Value上。
Automapper默认匹配了Get前缀,如果不需要可以清除:
cfg.ClearPrefixes();//清除所有前缀
使用ShouldMapField
和ShouldMapProperty
cfg.ShouldMapField = fi => false;
cfg.ShouldMapProperty = pi =>pi.GetMethod != null && (pi.GetMethod.IsPublic || pi.GetMethod.IsPrivate);
默认所有public的field和property都会被map,也会map private 的setter,但是不会map整个property都是internal/private
的属性。
默认是调用的时候才编译映射,但是可以要求AutoMapper提前编译,但是可能会花费点时间:
var configuration = new MapperConfiguration(cfg => {});
configuration.CompileMappings();
AutoMapper只会映射扁平的类,所以嵌套的类,继承的类,需要进行手动配置。成员名称不一致时,也要手动配置映射。
AutoMapper默认会自动映射以下类型,并且映射时会先清空dest对应成员的数据:
IEnumerable
IEnumerable<T>
ICollection
ICollection<T>
IList
IList<T>
List<T>
Arrays
这几个集合之间可以相互映射,如:mapper.Map<Source[], IEnumerable<Destination>>(sources);
ForMember
+MapFrom
// Configure AutoMapper
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<CalendarEvent, CalendarEventForm>()
.ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date))
.ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour))
.ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)));
将src的Date.Date
映射到dest的EventDate
成员上
Nested
)类和继承类映射某些成员可能是一个类,那么这个类也要配置映射。同理一个类的父类也要配置映射。
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<OuterSource, OuterDest>();
cfg.CreateMap<InnerSource, InnerDest>();
});
Include/IncludeBase
假如ChildSource继承ParentSource,ChildDestination继承ParentDestination。并且有这么一个业务,ParentSource src=new ChildSource()
需要把src转为ParentDestination
。
直接转的话肯定会有member丢失,所以要进行如下配置:
var configuration = new MapperConfiguration(c=> {
c.CreateMap<ParentSource, ParentDestination>()
.Include<ChildSource, ChildDestination>();
c.CreateMap<ChildSource, ChildDestination>();
});
或者也可以这么写:
CreateMap<ParentSource,ParentDestination>();
CreateMap<ChildSource,ChildDestination>().IncludeBase<ParentSource,ParentDestination>();
如果有几十个类都继承了ParentSource和ParentDestination,那么上述两种方法就太啰嗦了,可以这么写:
CreateMap<ParentSource,ParentDestination>().IncludeAllDerived();
CreaetMap<ChildSource,ChildDestination>();
更复杂的用法参考:Mapping-inheritance
如果dest构造函数的入参名和src的某个member一致,则不用手动配置,automapper会自动支持:
public int Value { get; set; }
}
public class SourceDto {
public SourceDto(int value) {
_value = value;
}
private int _value;
public int Value {
get { return _value; }
}
}
如果这里构造的入参不叫value而叫value2,则要进行如下配置:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, SourceDto>()
.ForCtorParam("value2", opt => opt.MapFrom(src => src.Value))
);
也可以禁用构造函数映射:
var configuration = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
也可以配置什么情况下不用构造函数映射:
var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = ci => !ci.IsPrivate);//不匹配私有构造函数
public class Src
{
public Customer Customer {get;set;}
public int GetTotal()
{
return 0;
}
}
public class Customer
{
public string Name {get;set;}
}
public class Dest
{
public string CustomerName {get;set;}
public int Total {get;set;}
}
则src可以自动映射成dest,包括CustomerName
和Total
字段。这种与手动配置cfg.CreateMap<Src,Dest>().ForMember(d=>d.CustomerName,opt=>opt.MapFrom(src=>src.Customer.Name))
然后进行映射的方式类似。
映射时AutoMapper发现,src里没有CustomerName
这个成员,则会将dest的CustomerName
按照PascalCase的命名方式拆分为独立的单词。所以CustomerName
会映射到src的Customer.Name
。如果想禁用这种自动映射,则调用cfg.DestinationMemberNamingConvention = new ExactMatchNamingConvention();
使用精确映射。
如果感觉AutoMapper的这种基于PascalCase命名拆分的自动映射没法满足你的需要,则还可以手动指定某些成员的映射:
class Source
{
public string Name { get; set; }
public InnerSource InnerSource { get; set; }
public OtherInnerSource OtherInnerSource { get; set; }
}
class InnerSource
{
public string Name { get; set; }
public string Description { get; set; }
}
class OtherInnerSource
{
public string Name { get; set; }
public string Description { get; set; }
public string Title { get; set; }
}
class Destination
{
public string Name { get; set; }
public string Description { get; set; }
public string Title { get; set; }
}
cfg.CreateMap<Source, Destination>().IncludeMembers(s=>s.InnerSource, s=>s.OtherInnerSource);
cfg.CreateMap<InnerSource, Destination>(MemberList.None);
cfg.CreateMap<OtherInnerSource, Destination>();
var source = new Source { Name = "name", InnerSource = new InnerSource{ Description = "description" }, OtherInnerSource = new OtherInnerSource{ Title = "title",Description="descpripiton2" } };
var destination = mapper.Map<Destination>(source);
destination.Name.ShouldBe("name");
destination.Description.ShouldBe("description");
destination.Title.ShouldBe("title");
IncludeMembers
参数的顺序很重要,这也就是dest的Description
为“description”而不是“description2”的原因,因为InnerSource
的Description
属性最先匹配到了Destination
的Description
属性。
IncludeMembers
相当于把子类打平添加到了src里,并进行映射。
reverse mapping一般在CreateMap方法或者ForMember等方法之后,相当于src和dest根据你自己的配置可以相互映射,少写一行代码:
cfg.CreateMap<Order, OrderDto>().ReverseMap();
//等同于以下两句
cfg.CreateMap<Order,OrderDto>();
cfg.CreateMap<OrderDto,Order>();
如果还想对reverse map进行自定义(大多数情况下都不需要),则可以使用ForPath:
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.CustomerName, opt => opt.MapFrom(src => src.Customer.Name))
.ReverseMap()
.ForPath(s => s.Customer.Name, opt => opt.MapFrom(src => src.CustomerName));
注意:
如果reverse之前定义了一些诸如ForMember
之类的约束,这些约束是不会自动reverse的,需要手动配置。以下代码配置了不管从Order
映射到OrderDto
还是从OrderDto
映射到Order
,都忽略CustomerName
属性。
cfg.CreateMap<Order, OrderDto>()
.ForMember(d => d.CustomerName, opt => opt.Ignore())
.ReverseMap()
.ForMember(d => d.CustomerName, opt => opt.Ignore())
(C#称作特性,Java叫注解)
[AutoMap(typeof(Order))]
public class OrderDto {}
等同于CreateMap<Order,OrderDto>()
。然后配置的时候用AddMaps方法:
var configuration = new MapperConfiguration(cfg => cfg.AddMaps("MyAssembly"));
var mapper = new Mapper(configuration);
注解里还有如下参数供设置:
ReverseMap (bool)
ConstructUsingServiceLocator (bool)
MaxDepth (int)
PreserveReferences (bool)
DisableCtorValidation (bool)
IncludeAllDerived (bool)
TypeConverter (Type)
映射注解的更多信息参考:Attribute-mapping
默认就支持,不用手动CreateMap
public class Source<T> {}
public class Destination<T> {}
var configuration = new MapperConfiguration(cfg => cfg.CreateMap(typeof(Source<>), typeof(Destination<>)));
注意:CreateMap
不需要传具体的T
IQueryable
(与EF等ORM配合使用)需要安装nuget包:AutoMapper.EF6
这个功能存在的意义是为了解决一些orm框架返回的是IQueryable
类型,使用一般的mapper.Map做转换时,会查询出来整行数据,然后再挑选出来某些字段做映射,会降低性能的问题。解决方法是使用ProjectTo
:
public class OrderLine
{
public int Id { get; set; }
public int OrderId { get; set; }
public Item Item { get; set; }
public decimal Quantity { get; set; }
}
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrderLineDTO
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Item { get; set; }
public decimal Quantity { get; set; }
}
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<OrderLine, OrderLineDTO>()
.ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)));
public List<OrderLineDTO> GetLinesForOrder(int orderId)
{
using (var context = new orderEntities())
{
return context.OrderLines.Where(ol => ol.OrderId == orderId)
.ProjectTo<OrderLineDTO>(configuration).ToList();
}
}
//这样查Item表的时候,只会select name字段。
TBD
大多数情况下都不需要进行此项的配置。考虑以下两个枚举:
public enum Source
{
Default = 0,
First = 1,
Second = 2
}
public enum Destination
{
Default = 0,
Second = 2
}
如果想把Source.First
映射到Destination.Default
上,则需要安装AutoMapper.Extensions.EnumMapping nuget包,然后进行如下配置:
internal class YourProfile : Profile
{
public YourProfile()
{
CreateMap<Source, Destination>()
.ConvertUsingEnumMapping(opt => opt
// optional: .MapByValue() or MapByName(), without configuration MapByValue is used
.MapValue(Source.First, Destination.Default)
)
.ReverseMap();
}
}
默认情况下AutoMapper.Extensions.EnumMapping 会将源枚举里所有的数据根据枚举值或枚举名称映射到目标枚举上。如果找不到且启用了EnumMappingValidation
,则会抛出异常。
当从可空类型与不可空类型相互转换时,当string
与int
等转换时,就需要自定义类型转换。一般有以下三种方法:
void ConvertUsing(Func<TSource, TDestination> mappingFunction);
void ConvertUsing(ITypeConverter<TSource, TDestination> converter);
void ConvertUsing<TTypeConverter>() where TTypeConverter : ITypeConverter<TSource, TDestination>;
简单情景下使用第一种,复杂情景下自定义类去实现ITypeConverter
接口。综合举例如下:
public class Source
{
public string Value1 { get; set; }
public string Value2 { get; set; }
public string Value3 { get; set; }
}
public class Destination
{
public int Value1 { get; set; }
public DateTime Value2 { get; set; }
public Type Value3 { get; set; }
}
public void Example()
{
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<string, int>().ConvertUsing(s => Convert.ToInt32(s));
cfg.CreateMap<string, DateTime>().ConvertUsing(new DateTimeTypeConverter());
cfg.CreateMap<string, Type>().ConvertUsing<TypeTypeConverter>();
cfg.CreateMap<Source, Destination>();
});
configuration.AssertConfigurationIsValid();
var source = new Source
{
Value1 = "5",
Value2 = "01/01/2000",
Value3 = "AutoMapperSamples.GlobalTypeConverters.GlobalTypeConverters+Destination"
};
Destination result = mapper.Map<Source, Destination>(source);
result.Value3.ShouldEqual(typeof(Destination));
}
public class DateTimeTypeConverter : ITypeConverter<string, DateTime>
{
public DateTime Convert(string source, DateTime destination, ResolutionContext context)
{
return System.Convert.ToDateTime(source);
}
}
public class TypeTypeConverter : ITypeConverter<string, Type>
{
public Type Convert(string source, Type destination, ResolutionContext context)
{
return Assembly.GetExecutingAssembly().GetType(source);
}
}
MapFrom
自定义值解析(Value Resolve )针对的是某一个map的某一个member,有3种写法:
MapFrom<TValueResolver>
MapFrom(typeof(CustomValueResolver))
MapFrom(new CustomResolver())
public class Source
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
public class Destination
{
public int Total { get; set; }
}
public class CustomResolver : IValueResolver<Source, Destination, int>
{
public int Resolve(Source source, Destination destination, int member, ResolutionContext context)
{
//可以添加其他逻辑
return source.Value1 + source.Value2;
}
}
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total, opt => opt.MapFrom<CustomResolver>()));
configuration.AssertConfigurationIsValid();
var source = new Source
{
Value1 = 5,
Value2 = 7
};
var result = mapper.Map<Source, Destination>(source);
result.Total.ShouldEqual(12);
这种与一般的MapFrom(src=>src.Value1+src.Value2)
区别是可以添加更加复杂的逻辑。
如果想要一个更通用的CustomResolver
,不管src和dest是什么类型的都能用,则可以实现IValueResolver<object,object,int>
接口。但是这个resolver里没法获取dest的value,如果有这种需要的话,可以实现ImemberValueResolver
接口,进行更细粒度的控制。
只支持传入键值对格式的数据
cfg.CreateMap<Source, Dest>()
.ForMember(dest => dest.Foo, opt => opt.MapFrom((src, dest, destMember, context) => context.Items["Foo"]));
mapper.Map<Source, Dest>(src, opt => opt.Items["Foo"] = "Bar");
Conditions
和Preconditions
)符合某些条件时才映射
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Foo,Bar>()
.ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0)));
});
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Foo,Bar>()
.ForMember(dest => dest.baz, opt => {
opt.PreCondition(src => (src.baz >= 0));
opt.MapFrom(src => {
//有了Precondition,这里的操作有可能不执行,节省资源
});
});
});
Condition()
方法会在MapFrom
方法后执行,而Preconditions
会在MapFrom
前执行
作用:如果src为null的话,就给dest一个默认值
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");
ValueTransformers
映射点缀(自己起的名字)作用:在赋值的时候,我想额外在值上加点东西
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!!!");
可以应用到全局、某个Profile、某个Map或某个member。
MapAction
可以提前配置:
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Dest>()
.BeforeMap((src, dest) => src.Value = src.Value + 10)
.AfterMap((src, dest) => dest.Name = "John");
});
也可以在map时进行配置:
int i = 10;
mapper.Map<Source, Dest>(src, opt => {
opt.BeforeMap((src, dest) => src.Value = src.Value + i);
opt.AfterMap((src, dest) => dest.Name = HttpContext.Current.Identity.Name);
});
MapAction也可以创建全局的。
详细用法:Before-and-after-map-actions
var configuration = new MapperConfiguration(cfg => {
cfg.AllowNullCollections = true;
cfg.CreateMap<Source, Destination>();
});
这个功能可以配置成全局的、某个profile的或某个member的。
CreateMap<SRC,DEST>
,创建映射时前后书写顺序不重要。AutoMapper.Collection
。NameAAA
,则名为NameAAA
的field,与名为NameAAA
的property,与名为GetNameAAA
的方法,三者之间可以自动相互映射。ToString
方法。对于那些不支持的类型转换,需要自己定义Type Converter
。ITypeConverter
、IValueConverter
、IValueResolver
、IMemberValueResover
好像都可以实现,我应该用哪个?这几个的释义如下:
Func<TSource, TDestination, TDestination>
Func<TSource, TDestination, TDestinationMember>
Func<TSource, TDestination, TSourceMember, TDestinationMember>
Func<TSourceMember, TDestinationMember>
这四个的使用方式都是使用ConvertUsing()
方法,区别是type converter 是针对全局的,其它三个是针对某个member的。 入参出参也不一样。
CreateMap<Src,Dest>()
?虽然AutoMapper的官方文档,一直都写着在映射之前要先用CreateMap
方法进行配置。但在实际使用过程中,我发现并不是任何时候都需要先配置才能用。
假如dest里的每一个member(属性、字段、Get方法)都能在src里找得到,则不需要额外的配置,即下属代码也可以正常运行:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class Person2
{
public string Name { get; set; }
public int? Age { get; set; }
public DateTime BirthTime { get; set; }
}
public class NormalProfile : Profile
{
public NormalProfile()
{
//CreateMap<Person2, Person>();//
}
}
var cfg = new MapperConfiguration(c =>
{
c.AddProfile<NormalProfile>();
});
//cfg.AssertConfigurationIsValid();
var mapper = cfg.CreateMapper();
var s3 = mapper.Map<Person>(new Person2 { Name = "Person2" });
ReverseMap
到底能Reverse哪些东西?Customer.Name
与CustomerName
的映射一样。即:CreateMap不需要额外配置正向就能映射的,那 ReverseMap也可以自动反向映射。opt.MapForm()
操作可以被reverse,如CreateMap<Person2, Person>().ForMember(dest => dest.Name2, opt => opt.MapFrom(src => src.Name)).ReverseMap();
,当从Person映射到Person2的时候,Name2也可以直接映射到Name上。opt.Ignore()
反向映射,即CreateMap<Person2, Person>().ForMember(dest => dest.Name, opt => opt.Ignore()).ReverseMap()
支持Person2->Person时,忽略Name属性,但是从Person->Person2时,不会忽略,如果要忽略的话,还需要再加上.ForMember(dest => dest.Name, opt => opt.Ignore())
。CreateMap<src,dest>().ForMember(dest=>dest.AAA,opt=>opt.Ignore())
,跳过成员AAA的映射。ShouldMapField
委托跳过某些字段,使用ShouldMapProperty
委托跳过某些属性。public class NormalProfile : Profile
{
public NormalProfile()
{
ShouldMapField = fi => false;
CreateMap<Person2, Person>().ReverseMap();
}
}
或者
var cfg = new MapperConfiguration(c =>
{
c.AddProfile<XXXXProfile>();
c.ShouldMapField = fi => false;
});