前言
在AutoMapper未出世前,对象与对象之间的映射,我们只能通过手动为每个属性一一赋值,时间长了不仅是我们而且老外也觉得映射代码很无聊啊。这个时候老外的所写的强大映射库AutoMapper横空出世,AutoMapper是一个对象映射库, 它提供简单的类型配置,以及简单的映射测试。对象映射通过将一种类型的输入对象转换为不同类型的输出对象而起作用。项目之前有用过,但是对其了解不够透彻映射时有时候会抛异常,后来弃之,本节我们来详细了解下AutoMapper映射库。
AutoMapper基础版
在AutoMapper中创建映射配置有两种方式。一种是通过实例化MapperConfiguration类来配置,一种是通过类Mapper中的静态方法Initialize来配置,下面我们来看看。
public class User { public int Id { get; set; } public int Age { get; set; } public string Name { get; set; } } public class UserDTO { public int Id { get; set; } public int Age { get; set; } public string Name { get; set; } }
static void Main(string[] args) { var user = new User() { Id = 1, Age = 10, Name = "Jeffcky" }; var config = new MapperConfiguration(cfg => cfg.CreateMap()); //或者Mapper.Initialize(cfg => cfg.CreateMap ()); var mapper = config.CreateMapper(); //或者var mapper = new Mapper(config); //最终调用Map方法进行映射 var userDTO = mapper.Map(user); Console.ReadKey(); }
在Map映射方法中有两个参数,我们通俗讲则是从一个映射到另一个对象,在AutoMapper中将其称为映射源和映射目标。
关于本节映射都通过如下静态方法来实现,简单粗暴。
Mapper.Initialize(cfg => cfg.CreateMap()); var userDTO = Mapper.Map (user);
接下来我们再来看若映射源为空,那么是否会进行映射,还是抛异常呢?
static void Main(string[] args) { User user = null; Mapper.Initialize(cfg => cfg.CreateMap()); var userDTO = Mapper.Map (user); Console.ReadKey(); }
到此我们总结出一点:AutoMapper将映射源映射到目标时,AutoMapper将忽略空引用异常。 这是AutoMapper默认设计。
是不是到此关于AutoMapper就讲完了呢?童鞋想想所有场景嘛,这个只是最简单的场景,或者天马行空想想其他问题看看AutoMapper支持不,比如我想想,AutoMapper对属性大小写是否敏感呢?想完就开干啊。我们将User对象属性全部改为小写:
public class User { public int id { get; set; } public int age { get; set; } public string name { get; set; } }
static void Main(string[] args) { var user = new User() { id = 1, age = 10, name = "Jeffcky" }; Mapper.Initialize(cfg => cfg.CreateMap()); var userDTO = Mapper.Map (user); Console.ReadKey(); }
到这里我们又可以总结出一点:AutoMapper从映射源到映射目标时不区分大小写。
AutoMapper中级版
我们讲完基础版,接下来来进入中级版看看AutoMapper到底有多强,磕不屎你哟。是否支持继承映射哎。
public class Base { public int Id { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } } public class User : Base { public int Age { get; set; } public string Name { get; set; } }
public class UserDTO { public int Id { get; set; } public DateTime CreatedTime { get; set; } public DateTime ModifiedTime { get; set; } public int Age { get; set; } public string Name { get; set; } }
var user = new User() { Id = 1, Age = 10, Name = "Jeffcky", CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now }; Mapper.Initialize(cfg => cfg.CreateMap()); var userDTO = Mapper.Map (user);
好了,看来也是支持的,我们总结来一个:AutoMapper从映射源到映射目标支持继承。讲完关于类的继承,我们来看看复杂对象,这下AutoMapper想必要有点挑战了吧。
public class Address { public string City { get; set; } public string State { get; set; } public string Country { get; set; } } public class AuthorModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public Address Address { get; set; } }
public class AuthorDTO { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string City { get; set; } public string State { get; set; } public string Country { get; set; } }
static void Main(string[] args) { var author = new AuthorModel() { Id = 1, FirstName = "Wang", LastName = "Jeffcky", Address = new Address() { City = "深圳", State = "1", Country = "中国" } }; Mapper.Initialize(cfg => cfg.CreateMap()); var authorDTO = Mapper.Map (author); Console.ReadKey(); }
哇喔,我说AutoMapper还能有这么智能,那还要我们程序员干嘛,在AuthorDTO中我们将Address扁平化为简单属性,所以此时利用Map不再是万能的,我们需要手动在创建映射配置时通过ForMember方法来自定义指定映射属性来源,从映射源中的Address复杂对象属性到AuthorDTO中属性上。
var author = new AuthorModel() { Id = 1, FirstName = "Wang", LastName = "Jeffcky", Address = new Address() { City = "深圳", State = "1", Country = "中国" } }; Mapper.Initialize(cfg => cfg.CreateMap() .ForMember(d => d.City, o => o.MapFrom(s => s.Address.City)) .ForMember(d => d.State, o => o.MapFrom(s => s.Address.State)) .ForMember(d => d.Country, o => o.MapFrom(s => s.Address.Country)) ); var authorDTO = Mapper.Map (author);
如上所给片段代码,对于AuthorDTO中的City属性,我们指定其值来源于映射源中复杂属性Address中的City,其余同理,同时对于其他在相同层次上的属性不会进行覆盖。
默认情况下AutoMapper会将同名且不区分大小写的属性进行映射,比如对于有些属性为了节省传输流量且完全不需要用到的属性,我们压根没必要进行映射,此时AutoMapper中有Ignore方法来忽略映射,如下代码片段将忽略对属性Id的映射。
Mapper.Initialize(cfg => cfg.CreateMap() .ForMember(d => d.Id, o => o.Ignore()) );
到此我们又可以来一个总结:AutoMapper支持从映射源到映射目标的扁平化。实际上AutoMapper支持扁平化映射,但是前提是遵守AutoMapper映射约定才行,我们走一个。
public class Customer { public Company Company { get; set; } } public class Company { public string Name { get; set; } } public class CustomerDTO { public string CompanyName { get; set; } }
static void Main(string[] args) { var customer = new Customer() { Company = new Company() { Name = "腾讯" } }; Mapper.Initialize(cfg => { cfg.CreateMap(); }); var customerDTO = Mapper.Map (customer); Console.ReadKey(); }
你看我们什么都没做,结果同样还是映射到了目标类中,不过是遵守了AutoMapper的映射约定罢了,看到这个想必大家就马上明白过来了。如果扁平化映射源类,若想AutoMapper依然能够自动映射,那么映射目标类中的属性必须是映射源中复杂属性名称加上复杂属性中的属性名称才行,因为AutoMapper会深度搜索目标类,直到找到匹配的属性为止。下面我们再来看看集合映射。
public class Customer { public int Id { get; set; } public string Name { get; set; } public IEnumerableOrders { get; set; } } public class Order { public int Id { get; set; } public string TradeNo { get; set; } public int TotalFee { get; set; } } public class CustomerDTO { public int Id { get; set; } public string Name { get; set; } public IEnumerable OrderDTOs { get; set; } } public class OrderDTO { public int Id { get; set; } public string TradeNo { get; set; } public int TotalFee { get; set; } }
上述Customer对象中有Order的集合属性,所以怕AutoMapper是映射不了,我们手动配置一下,如下:
static void Main(string[] args) { var customer = new Customer() { Id = 1, Name = "Jeffcky", Orders = new List() { new Order() { Id =1, TotalFee = 10, TradeNo = "20172021690326" } } }; Mapper.Initialize(cfg => cfg.CreateMap () .ForMember(d => d.OrderDTOs, o => o.MapFrom(s => s.Orders)) ); var customerDTO = Mapper.Map (customer); Console.ReadKey(); }
喔,抛出异常了,哈哈,果然AutoMapper还有不支持的,果断弃之(我们项目当时就是一直出这样的问题于是乎弃用了)。慢着,老铁。利用AutoMapper映射大部分情况下都会遇到如上异常,所以我们来分析下,在AutoMapper中,当它偶遇一个接口的目标对象时,它会自动生成动态代理类,怎么感觉好像说到EntityFramework了。 当映射到不存在的映射目标时,这就是内部设计的行为了。 然而然而,我们映射目标类却存在啊,于是乎我修改了AutoMapper映射,将Order到OrderDTO也进行映射配置,然后在配置映射Customer对象再指定Order集合属性,我们试试。
Mapper.Initialize(cfg => { cfg.CreateMap(); cfg.CreateMap () .ForMember(d => d.OrderDTOs, o => o.MapFrom(s => Mapper.Map , IList >(s.Orders))); }); var customerDTO = Mapper.Map (customer);
老铁妥妥没毛病,通过此种方式即使嵌套多层依然也是能够解析,只不过我们得手动多几个配置罢了不是,这里我们又来一个结论:在映射复杂对象中的集合属性时,我们需要配置集合属性的映射,然后在复杂对象中再次映射集合属性。
2017-10-13补充
在写Demo项目时发现还有一种很常见的场景,但是若不注意也会映射出错,下面我们来看看。
public class User { public string UserName { get; set; } public string Email { get; set; } public string Password { get; set; } public virtual UserProfile UserProfile { get; set; } } public class UserProfile { public string FirstName { get; set; } public string LastName { get; set; } public string Address { get; set; } public virtual User User { get; set; } }
public class UserDTO { public Int64 ID { get; set; } [Display(Name ="First Name")] public string FirstName { get; set; } [Display(Name="Last Name")] public string LastName { get; set; } public string Address { get; set; } [Display(Name="User Name")] public string UserName { get; set; } public string Email { get; set; } public string Password { get; set; } [Display(Name ="Added Date")] public DateTime AddedDate { get; set; } }
同样是扁平化,接下来我们再来进行映射
CreateMap() .ForMember(d => d.FirstName, m => m.MapFrom(f => f.UserProfile.FirstName)) .ForMember(d => d.LastName, m => m.MapFrom(f => f.UserProfile.LastName)) .ForMember(d => d.Address, m => m.MapFrom(f => f.UserProfile.Address)); CreateMap () .ForMember(d => d.UserProfile.FirstName, m => m.MapFrom(f => f.FirstName)) .ForMember(d => d.UserProfile.LastName, m => m.MapFrom(f => f.LastName)) .ForMember(d => d.UserProfile.Address, m => m.MapFrom(f => f.Address));
此时我们当然可以利用AfterMap来实现,但是还是有其他解决方案,如下:
CreateMap() .ForMember(d => d.UserProfile, m => m.MapFrom(f => f)); CreateMap () .ForMember(d => d.FirstName, m => m.MapFrom(f => f.FirstName)) .ForMember(d => d.LastName, m => m.MapFrom(f => f.LastName)) .ForMember(d => d.Address, m => m.MapFrom(f => f.Address));
AutoMapper高级版
AutoMapper太强大了,我给跪了,强大到这篇幅不够,得手动下拉滚动条继续磕。废话少说,我们再来看看AutoMapper使用高级版,自定义值解析,动态对象映射、类型转换等。
自定义值解析
AutoMapper支持自定义解析,只不过我们需要实现IValueResolver接口才行,下面我们来看看。
public class Customer { public bool VIP { get; set; } } public class CustomerDTO { public string VIP { get; set; } }
实现IValueResolver接口,对映射源加以判断返回映射目标中的字符串。
public class VIPResolver : IValueResolverstring> { public string Resolve(Customer source, CustomerDTO destination, string destMember, ResolutionContext context) { return source.VIP ? "Y" : "N"; } }
然后在映射配置时使用ResolveUsing来实现上述自定义解析,使用方式有如下两种。
var customer = new Customer() { VIP = true }; Mapper.Initialize(cfg => { cfg.CreateMap() .ForMember(cv => cv.VIP, m => m.ResolveUsing ()); }); //或者 //Mapper.Initialize(cfg => //{ // cfg.CreateMap () // .ForMember(cv => cv.VIP, m => m.ResolveUsing(new VIPResolver())); //}); var customerDTO = Mapper.Map(customer);
动态对象映射
public class Customer { public int Id { get; set; } public string Name { get; set; } }
dynamic customer = new ExpandoObject(); customer.Id = 5; customer.Name = "Jeffcky"; Mapper.Initialize(cfg => { }); var result = Mapper.Map(customer); dynamic foo2 = Mapper.Map (result);
类型转换
关于上述自定义值解析,我们同样可以用类型转换类实现,在AutoMapper中存在ConvertUsing方法,该方法类似于C#中的投影一样,如下:
Mapper.Initialize(cfg => { cfg.CreateMap() .ConvertUsing(s => new CustomerDTO() { VIP = s.VIP ? "Y" : "N" }); });
或者
public class CustomTypeConverter : ITypeConverter{ public CustomerDTO Convert(Customer source, CustomerDTO destination, ResolutionContext context) { return new CustomerDTO { VIP = source.VIP ? "Y" : "N", }; } }
Mapper.Initialize(cfg => { cfg.CreateMap() .ConvertUsing(new CustomTypeConverter()); });
AutoMapper太强大了,上述已经给出大部分我们基本上会用到的场景,AutoMapper还支持依赖注入,同时最爽的是有了AutoMapper.QueryableExtensions扩展方法,这针对使用EF的童鞋简直是福音啊。 通过ProjectTo方法即可映射从数据库查询出的IQueryable类型数据。
IQueryablecustomers = null; var customersDTO = customers.ProjectTo ();
总结
AutoMapper强大到给跪了,目前该项目已被.NET基金会所支持,看过的,路过的,没用过的,赶紧走起用起来啊,有时间还会更新AutoMapper其他用途,想必上述场景已经够我们用了吧,如果你觉得不够用,请私信我,我再加上啊。