AutoMapper
- 初步認識AutoMapper
- 前言
- 手動映射
- 使用AutoMapper
- 創建映射
- Conventions
- 映射到一個已存在的實例對象
前言
通常在一個應用程序中,我們開發人員會在兩個不同的類型對象之間傳輸數據,通常我們會用DTOs(數據傳輸對象),View Models(視圖模型),或者直接是一些從一個service或者Web API的一些請求或應答對象。一個常見的需要使用數據傳輸對象的情況是,我們想把屬於一個對象的某些屬性值賦值給另一個對象的某些屬性值,但是問題是,這個兩個對象可能並不是完全匹配的,比如,兩者之間的屬性類型,名稱等等,是不一樣的,或者我們只是想把一個對象的一部分屬性值賦值給另一個對象。
手動映射
首先,讓我們來看下之前的處理方式,我們通過以下這個例子來直觀感受這種方式,我們創建了以下三個類:
public class Author
{
public string Name { get; set; }
}
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
public class BookViewModel
{
public string Title { get; set; }
public string Author { get; set; }
}
為了創建Book對象實例的一個View Model對象實例-BookViewModel對象實例,我們需要寫如下代碼:
BookViewModel model = new BookViewModel
{
Title = book.Title,
Author = book.Author.Name
}
上面的例子相當的直觀了,但是問題也隨之而來了,我們可以看到在上面的代碼中,如果一旦在Book對象里添加了一個額外的字段,而后想在前台頁面輸出這個字段,那么就需要去在項目里找到每一處有這樣轉換字段的地方,這是非常繁瑣的。另外,BookViewModel.Author是一個string類型的字段,但是Book.Author屬性卻是Author對象類型的,我們用的解決方法是通過Book.Auther對象來取得Author的Name屬性值,然后再賦值給BookViewModel的Author屬性,這樣看起行的通,但是想一想,如果打算在以后的開發中把Name拆分成兩個-FisrtName和LastName,那么,呵呵,我們得去把原來的ViewModel對象也拆分成對應的兩個字段,然后在項目中找到所有的轉換,然后替換。
那么有什么辦法或者工具來幫助我們能夠避免這樣的情況發生呢?AutoMapper正是符合要求的一款插件。
使用AutoMapper
到現在,確切的說,AutoMapper的安裝使用非常非常的便捷,就如同傻瓜照相機那樣。你只需要從Nuget上下載AutoMapper的包到你的應用程序里,然后添加對AutoMapper命名空間的引用,然后你就可以在你的項目里隨意使用它了。以下就是一個非常簡單的是例子:
AutoMapper.Mapper.CreateMap<Book, BookViewModel>();
var model = AutoMapper.Mapper.Map<BookViewModel>(book);
使用AutoMappeer的好處是顯而易見的,首先,不再需要我們去對DTO實例的屬性一一賦值,然后無論你在Book對象或者BookViewModel對象里加了一個或者更多的字段,那都不會影響這個段映射的代碼,我不再需要去找到每一處轉換的地方去更改代碼,你的程序會像之前正常運轉。
不過,還是有個問題並沒有得到很好的解決,這也是在AutoMapper文檔上缺失的,為把Book.Athor.Name字段賦值給BookViewModel.Author字段,需要在每一處需要執行映射的代碼地方,同時創建一個如下的顯示轉換申明代碼,所以如果有很多處轉換的話,那么我們就會寫很多重復的這幾行代碼:
AutoMapper.Mapper.CreateMap<Book, BookViewModel>()
.ForMember(dest => dest.Author,
opts => opts.MapFrom(src => src.Author.Name));
所以我們該如何正確的創建映射呢?方式有很多,我這邊說下在ASP.NET MVC的程序里如何處理。
在微軟的ASP.NET MVC程序中,它提供了一個Global.asax文件,這個文件里可以放置一些全劇配置,上面對於把Book.Athor.Name字段賦值給BookViewModel.Author字段這個映射配置放置在這個文件里面,那么這段代碼只會跑一次但是所有轉換的地方都能正確的轉換Book.Athor.Name為BookViewModel.Author。當然,Global.asax文件中不建議放很復雜的代碼,因為這是ASP.NET程序的入口,一檔這個文件里出錯,那么整個程序就會over。配置代碼可以以這樣的形式寫,創建一個AutoMapper的配置類:
public static class AutoMapperConfig
{
public static void RegisterMappings()
{
AutoMapper.Mapper.CreateMap<Book, BookViewModel>()
.ForMember(dest => dest.Author,
opts => opts.MapFrom(src => src.Author.Name));
}
}
然后再Global文件注冊這個類:
protected override void Application_Start(object sender, EventArgs e)
{
AutoMapperConfig.RegisterMappings();
}
創建映射
所有的映射是有CreateMap方法來完成的:
AutoMapper.Mapper.CreateMap<SourceClass, >();
需要注意的是:這種方式是單向的匹配,即在在創建了上面的映射了之后我們可以在程序里從一個SourceClass實例得到一個DestinationClass類型的對象實例:
var destinationClass= AutoMapper.Mapper.Map<DestinationClass>(sourceClass);
但是如果嘗試從DestinationClass映射到一個SourceClass,我們到的是一個錯誤信息:
var book = AutoMapper.Mapper.Map<Book>(bookViewModel);
幸運的是,AutoMapper已經考慮到這個問題了,它提供了ReverseMap方法:
AutoMapper.Mapper.CreateMap<Book, BookViewModel>().ReverseMap();
使用了這個方式后你就可以從Book創建BookViewModel,同時也可以從BookViewModel創建Book對象實例。
Conventions
AutoMapper之所以能和任何一種集合類型產生交集,是由於它可以配置各種Conventions來完成一個類型到另一個類型的映射。最基本的一點就是兩個映射類型之間的字段名稱需要相同。例如一下的一個例子:
public class Book
{
public string Title { get; set; }
}
public class NiceBookViewModel
{
public string Title { get; set; }
}
public class BadBookViewModel
{
public string BookTitle { get; set; }
}
如果從Book映射到NiceBookViewModel,那么NiceBookBiewModel的Title屬性會被正確設置,但是如果將Book映射為BadBookViewModel,那么BookTitle的屬性值將會為NULL值。所以這種情況下,AutoMapper看起來失效了,不過,幸運的是,AutoMapper已經預先考慮到這種情況了,AutoMapper可以通過投影的方式來正確的映射BadBookViewModel和Book,只需要一行代碼:
AutoMapper.Mapper.CreateMap<Book, BadBookViewModel>()
.ForMember(dest => dest.BookTitle,
opts => opts.MapFrom(src => src.Title));
一種比較復雜的情況的是,當一個類型中引用了另一個類型的作為其一個屬性,例如:
public class Author
{
public string Name { get; set; }
}
public class Book
{
public string Title { get; set; }
public Author Author { get; set; }
}
public class BookViewModel
{
public string Title { get; set; }
public string Author { get; set; }
}
雖然Book和BookViewModel都有這一個Author的屬性子都,但是它們的類型是不同,所有如果使用AutoMapper來映射Book的Author到BookViewModel的Author,我們得到的還是一個NULL值。對於這種以另一個類型為屬性的映射,AutoMapper內置默認的有個Conventions是會這個的屬性名加上這個屬性的類型里的屬性名稱映射到目標類型具有相同名稱的字段,即如果在BookViewModel里有一個叫AuthorName的,那么我們可以得到正確的Name值。但是如果我們既不想改名稱,又想能正確的映射,怎么辦呢?Convention就是為此而誕生的:
AutoMapper.Mapper.CreateMap<Book, BookViewModel>()
.ForMember(dest => dest.Author,
opts => opts.MapFrom(src => src.Author.Name));
對於AutoMapper,它提供的Conventions功能遠不止這些,對於更加復雜的情形,它也能夠應對,例如當Author類型的字段有兩個屬性組成:
public class Author
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
但是我們仍然只想映射到BookViewModel的一個字段,為此,我們可以這么做:
AutoMapper.Mapper.CreateMap<Book, BookViewModel>()
.ForMember(dest => dest.Author,
opts => opts.MapFrom(
src => string.Format("{0} {1}",
src.Author.FirstName,
src.Author.LastName)));
還可以更加復雜,例如:
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
public class PersonDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
如果從Person映射為PersonDTO,我們只要想上面一樣的做飯就可以了。但是如果這個時候我們要做的是把PersonDTO映射為Book實體呢?代碼其實是差不多的:
AutoMapper.Mapper.CreateMap<PersonDTO, Person>()
.ForMember(dest => dest.Address,
opts => opts.MapFrom(
src => new Address
{
Street = src.Street,
City = src.City,
State = src.State,
ZipCode = src.ZipCode
}));
所以,我們在Convertion中構建了一個新的Address的實例,然后賦值給Book的Address的屬性。
有時候,我們可能創建了不止一個DTO來接受映射的結果,例如,對於Address,我們同樣創建了一個AddressDTO:
public class AddressDTO
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public class PersonDTO
{
public string FirstName { get; set; }
public string LastName { get; set; }
public AddressDTO Address { get; set; }
}
這個時候如果我們直接嘗試把Person映射為PersonDTO,會報錯,映射AutoMapper並不知道Address和AddressDTO之間的映射關系,我們需要手動創建:
AutoMapper.Mapper.CreateMap<PersonDTO, Person>();
AutoMapper.Mapper.CreateMap<AddressDTO, Address>();
映射到一個已存在的實例對象
之前我們都是把映射得到的結果賦值給一個變量,AutoMapper提供了另外一種方式,它使得我們可以直接映射兩個已存在的實例。
之前的做法:
AutoMapper.Mapper.CreateMap<SourceClass, DestinationClass>();
var destinationObject = AutoMapper.Mapper.Map<DestinatationClass>(sourceObject);
直接映射的做法:
AutoMapper.Mapper.Map(sourceObject, destinationObject);
AutoMapper也支持映射集合對象:
var destinationList = AutoMapper.Mapper.Map<List<DestinationClass>>(sourceList);
對於ICollectionIEnumerable的也是同樣適用。但是在用AutoMapper來實現內部的集合映射的時候,是非常非常不愉快的,因為AutoMapper會把這個集合作為一個屬性來映射賦值,而不是把內置的集合里的一行行內容進行映射,例如對於如下的一個例子:
public class Pet
{
public string Name { get; set; }
public string Breed { get; set; }
}
public class Person
{
public List<Pet> Pets { get; set; }
}
public class PetDTO
{
public string Name { get; set; }
public string Breed { get; set; }
}
public class PersonDTO
{
public List<PetDTO> Pets { get; set; }
}
我們在頁面上創建一個更新Pet類型的Name屬性的功能,然后提交更新,收到的數據差不多是這樣:
{
Pets: [
{ Name : "Sparky", Breed : null },
{ Name : "Felix", Breed : null },
{ Name : "Cujo", Breed : null }
]
}
這個時候如果我們去將Person映射為PersonDTO:
AutoMapper.Mapper.Map(person, personDTO);
我們得到將是一個全新的Pet的集合,即Name是更新后的數據,但是所有的Breed的值都將為NULL,這個不是所期望的結果。
很不幸的是,AutoMapper並沒有提供很好的解決方案。目前能做的一種方案就是用AutoMapper的Ignore方法忽略Pet的屬性的映射,然后我們自己去完成映射:
AutoMapper.Mapper.CreateMap<PersonDTO, Person>()
.ForMember(dest => dest.Pets,
opts => opts.Ignore());
AutoMapper.Mapper.Map(person, personDTO);
for (int i = 0; i < person.Pets.Count(); i++)
{
AutoMapper.Mapper.Map(person.Pets[i], personDTO.Pets[i]);
}
譯自:http://cpratt.co/using-automapper-getting-started/