前言
在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<User, UserDTO>()); //或者Mapper.Initialize(cfg => cfg.CreateMap<User, UserDTO>()); var mapper = config.CreateMapper(); //或者var mapper = new Mapper(config); //最終調用Map方法進行映射 var userDTO = mapper.Map<User, UserDTO>(user); Console.ReadKey(); }
在Map映射方法中有兩個參數,我們通俗講則是從一個映射到另一個對象,在AutoMapper中將其稱為映射源和映射目標。
關於本節映射都通過如下靜態方法來實現,簡單粗暴。
Mapper.Initialize(cfg => cfg.CreateMap<User, UserDTO>()); var userDTO = Mapper.Map<User, UserDTO>(user);
接下來我們再來看若映射源為空,那么是否會進行映射,還是拋異常呢?
static void Main(string[] args) { User user = null; Mapper.Initialize(cfg => cfg.CreateMap<User, UserDTO>()); var userDTO = Mapper.Map<User, UserDTO>(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<User, UserDTO>()); var userDTO = Mapper.Map<User, UserDTO>(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<User, UserDTO>()); var userDTO = Mapper.Map<User, UserDTO>(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<AuthorModel, AuthorDTO>()); var authorDTO = Mapper.Map<AuthorModel, AuthorDTO>(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<AuthorModel, AuthorDTO>() .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<AuthorModel, AuthorDTO>(author);
如上所給片段代碼,對於AuthorDTO中的City屬性,我們指定其值來源於映射源中復雜屬性Address中的City,其余同理,同時對於其他在相同層次上的屬性不會進行覆蓋。
默認情況下AutoMapper會將同名且不區分大小寫的屬性進行映射,比如對於有些屬性為了節省傳輸流量且完全不需要用到的屬性,我們壓根沒必要進行映射,此時AutoMapper中有Ignore方法來忽略映射,如下代碼片段將忽略對屬性Id的映射。
Mapper.Initialize(cfg => cfg.CreateMap<AuthorModel, AuthorDTO>() .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<Customer, CustomerDTO>(); }); var customerDTO = Mapper.Map<Customer, CustomerDTO>(customer); Console.ReadKey(); }
你看我們什么都沒做,結果同樣還是映射到了目標類中,不過是遵守了AutoMapper的映射約定罷了,看到這個想必大家就馬上明白過來了。如果扁平化映射源類,若想AutoMapper依然能夠自動映射,那么映射目標類中的屬性必須是映射源中復雜屬性名稱加上復雜屬性中的屬性名稱才行,因為AutoMapper會深度搜索目標類,直到找到匹配的屬性為止。下面我們再來看看集合映射。
public class Customer { public int Id { get; set; } public string Name { get; set; } public IEnumerable<Order> Orders { 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<OrderDTO> 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<Order>() { new Order() { Id =1, TotalFee = 10, TradeNo = "20172021690326" } } }; Mapper.Initialize(cfg => cfg.CreateMap<Customer, CustomerDTO>() .ForMember(d => d.OrderDTOs, o => o.MapFrom(s => s.Orders)) ); var customerDTO = Mapper.Map<Customer, CustomerDTO>(customer); Console.ReadKey(); }
喔,拋出異常了,哈哈,果然AutoMapper還有不支持的,果斷棄之(我們項目當時就是一直出這樣的問題於是乎棄用了)。慢着,老鐵。利用AutoMapper映射大部分情況下都會遇到如上異常,所以我們來分析下,在AutoMapper中,當它偶遇一個接口的目標對象時,它會自動生成動態代理類,怎么感覺好像說到EntityFramework了。 當映射到不存在的映射目標時,這就是內部設計的行為了。 然而然而,我們映射目標類卻存在啊,於是乎我修改了AutoMapper映射,將Order到OrderDTO也進行映射配置,然后在配置映射Customer對象再指定Order集合屬性,我們試試。
Mapper.Initialize(cfg => { cfg.CreateMap<Order, OrderDTO>(); cfg.CreateMap<Customer, CustomerDTO>() .ForMember(d => d.OrderDTOs, o => o.MapFrom(s => Mapper.Map<IList<Order>, IList<OrderDTO>>(s.Orders))); }); var customerDTO = Mapper.Map<Customer, CustomerDTO>(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<User, UserDTO>() .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<UserDTO, User>() .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<UserDTO, User>() .ForMember(d => d.UserProfile, m => m.MapFrom(f => f)); CreateMap<UserDTO, UserProfile>() .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 : IValueResolver<Customer, CustomerDTO, string> { 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<Customer, CustomerDTO>() .ForMember(cv => cv.VIP, m => m.ResolveUsing<VIPResolver>()); }); //或者 //Mapper.Initialize(cfg => //{ // cfg.CreateMap<Customer, CustomerDTO>() // .ForMember(cv => cv.VIP, m => m.ResolveUsing(new VIPResolver())); //}); var customerDTO = Mapper.Map<Customer, CustomerDTO>(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>(customer); dynamic foo2 = Mapper.Map<ExpandoObject>(result);
類型轉換
關於上述自定義值解析,我們同樣可以用類型轉換類實現,在AutoMapper中存在ConvertUsing方法,該方法類似於C#中的投影一樣,如下:
Mapper.Initialize(cfg => { cfg.CreateMap<Customer, CustomerDTO>() .ConvertUsing(s => new CustomerDTO() { VIP = s.VIP ? "Y" : "N" }); });
或者
public class CustomTypeConverter : ITypeConverter<Customer, CustomerDTO> { public CustomerDTO Convert(Customer source, CustomerDTO destination, ResolutionContext context) { return new CustomerDTO { VIP = source.VIP ? "Y" : "N", }; } }
Mapper.Initialize(cfg => { cfg.CreateMap<Customer, CustomerDTO>() .ConvertUsing(new CustomTypeConverter()); });
AutoMapper太強大了,上述已經給出大部分我們基本上會用到的場景,AutoMapper還支持依賴注入,同時最爽的是有了AutoMapper.QueryableExtensions擴展方法,這針對使用EF的童鞋簡直是福音啊。 通過ProjectTo方法即可映射從數據庫查詢出的IQueryable類型數據。
IQueryable<Customer> customers = null; var customersDTO = customers.ProjectTo<CustomerDTO>();
總結
AutoMapper強大到給跪了,目前該項目已被.NET基金會所支持,看過的,路過的,沒用過的,趕緊走起用起來啊,有時間還會更新AutoMapper其他用途,想必上述場景已經夠我們用了吧,如果你覺得不夠用,請私信我,我再加上啊。