- 初步認識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的安裝使用非常非常的便捷,就如同傻瓜照相機那樣。你隻需要從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對象執行個體。
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]);