一些orm框架,在用到Entity的時候有一些開源代碼用到了automapper(如:nopcommence),將數據對象轉成DTO。比如在ORM中,與數據庫交互用的Model模型是具有很多屬性變量方法神馬的。而當我們與其它系統(或系統中的其它結構)進行數據交互時,出於耦合性考慮或者安全性考慮或者性能考慮(總之就是各種考慮),我們不希望直接將這個Model模型傳遞給它們,這時我們會創建一個貧血模型來保存數據並傳遞。什么是貧血模型?貧血模型(DTO,Data Transfer Object)就是說只包含屬性什么的,只能保存必須的數據,沒有其它任何的多余的方法數據什么的,專門用於數據傳遞用的類型對象。在這個創建的過程中,如果我們手動來進行,就會看到這樣的代碼:
A a=new A();
a.X1=b.X1;
a.X2=b.X2;
...
...
...
return a;
此時,AutoMapper可以發揮的作用就是根據A的模型和B的模型中的定義,自動將A模型映射為一個全新的B模型。基於訪問性的控制或從模型本身上考慮。對外開放的原則是,盡量降低系統耦合度,否則內部一旦變更外部所有的接口都要跟隨發生變更;另外,系統內部的一些數據或方法並不希望外部能看到或調用。類似的考慮很多,只是舉個例子。系統設計的原則是高內聚低耦合,盡量依賴抽象而不依賴於具體。這里感覺automapper就是使數據庫實體對一個外部調用實體的轉換更簡便(不用一個屬性一個屬性的賦值)。
例如1:數據庫里面有用戶信息表,供別的系統調用,提供了數據接口。如果直接暴露了數據庫層的表結構的話,會對系統本身產生依賴。具體表現在,假定現在因為某種需要,為用戶信息增加了十個字段的信息,那么,如果不進行類型映射的話,會導致所有基於此用戶數據結構的模塊集體掛掉(接口約定變更)。而如果使用了映射的話,我們可以在內部進行轉換,保持原有接口不變並提供新的更全面的接口,這是保證系統的可維護性和可遷移性。
例如2:一個Web應用通過前端收集用戶的輸入成為Dto,然后將Dto轉換成領域模型並持久化到數據庫中。相反,當用戶請求數據時,我們又需要做相反的工作:將從數據庫中查詢出來的領域模型以相反的方式轉換成Dto再呈現給用戶。使用AutoMapper(一個強大的Object-Object Mapping工具),來實現這個轉換。
一 ,應用場景
先來看看我所虛擬的領域模型。這一次我定義了一個書店(BookStore):
public class BookStore { public string Name { get; set; } public List<Book> Books { get; set; } public Address Address { get; set; } }
書店有自己的地址(Address):
public class Address { public string Country { get; set; } public string City { get; set; } public string Street { get; set; } public string PostCode { get; set; } }
同時書店里放了N本書(Book):
public class Book { public string Title { get; set; } public string Description { get; set; } public string Language { get; set; } public decimal Price { get; set; } public List<Author> Authors { get; set; } public DateTime? PublishDate { get; set; } public Publisher Publisher { get; set; } public int? Paperback { get; set; } }
每本書都有出版商信息(Publisher):
public class Publisher { public string Name { get; set; } }
每本書可以有最多2個作者的信息(Author):
public class Author { public string Name { get; set; } public string Description { get; set; } public ContactInfo ContactInfo { get; set; } }
每個作者都有自己的聯系方式(ContactInfo):
public class ContactInfo { public string Email { get; set; } public string Blog { get; set; } public string Twitter { get; set; } }
差不多就是這樣了,一個有着層級結構的領域模型。 再來看看我們的Dto結構。 在Dto中我們有與BookStore對應的BookStoreDto:
public class BookStoreDto { public string Name { get; set; } public List<BookDto> Books { get; set; } public AddressDto Address { get; set; } }
其中包含與Address對應的AddressDto:
public class AddressDto { public string Country { get; set; } public string City { get; set; } public string Street { get; set; } public string PostCode { get; set; } }
以及與Book相對應的BookDto:
public class BookDto { public string Title { get; set; } public string Description { get; set; } public string Language { get; set; } public decimal Price { get; set; } public DateTime? PublishDate { get; set; } public string Publisher { get; set; } public int? Paperback { get; set; } public string FirstAuthorName { get; set; } public string FirstAuthorDescription { get; set; } public string FirstAuthorEmail { get; set; } public string FirstAuthorBlog { get; set; } public string FirstAuthorTwitter { get; set; } public string SecondAuthorName { get; set; } public string SecondAuthorDescription { get; set; } public string SecondAuthorEmail { get; set; } public string SecondAuthorBlog { get; set; } public string SecondAuthorTwitter { get; set; } }
注意到我們的BookDto”拉平了“整個Book的層級結構,一個BookDto里攜帶了Book及其所有Author、Publisher等所有模式的數據。正好我們來看一下Dto到Model的映射規則。
(1)BookStoreDto –> BookStore
BookStoreDto中的字段 | BookStore中的字段 |
Name | Name |
Books | Books |
Address | Address |
(2)AddressDto –> Address
AddressDto中的字段 | Address中的字段 |
Country | Country |
City | City |
Street | Street |
PostCode | PostCode |
(3)BookDto -> Book。 BookDto中的一些基本字段可以直接對應到Book中的字段。
BookDto中的字段 | Book中的字段 |
Title | Title |
Description | Description |
Language | Language |
Price | Price |
PublishDate | PublishDate |
Paperback | Paperback |
每本書至多有2個作者,在BookDto中分別使用”First“前綴和”Second“前綴的字段來表示。因此,所有FirstXXX字段都將映射成Book的Authors中的第1個Author對象,而所有SecondXXX字段則將映射成Authors中的第2個Author對象。
BookDto中的字段 | Book中的Authors中的第1個Author對象中的字段 |
FirstAuthorName | Name |
FirstAuthorDescription | Description |
FirstAuthorEmail | ContactInfo.Email |
FirstAuthorBlog | ContactInfo.Blog |
FirstAuthorTwitter | ContactInfo.Twitter |
注意上表中的ContactInfo.Email表示對應到Author對象的ContactInfo的Email字段,依次類推。類似的我們有:
BookDto中的字段 | Book中的Authors中的第2個Author對象中的字段 |
SecondAuthorName | Name |
SecondAuthorDescription | Description |
SecondAuthorEmail | ContactInfo.Email |
SecondAuthorBlog | ContactInfo.Blog |
SecondAuthorTwitter | ContactInfo.Twitter |
最后還有Publisher字段,它將對應到一個獨立的Publisher對象。
BookDto中的字段 | Publisher中的字段 |
Publisher | Name |
差不多就是這樣了,我們的需求是要實現這一大坨Dto到另一大坨的Model之間的數據轉換。
二,以Convention方式實現零配置的對象映射
在上一篇文章中我們構造出了完整的應用場景,包括我們的Model、Dto以及它們之間的轉換規則。下面開始我們的AutoMapper之旅了。
我們要做的只是將要映射的兩個類型告訴AutoMapper(調用Mapper類的Static方法CreateMap並傳入要映射的類型):
Mapper.CreateMap<AddressDto, Address>();
然后就可以交給AutoMapper幫我們搞定一切了:
AddressDto dto = new AddressDto { Country = "China", City = "Beijing", Street = "Dongzhimen Street", PostCode = "100001" }; Address address = Mapper.Map<AddressDto,Address>(Dto); address.Country.ShouldEqual("China"); address.City.ShouldEqual("Beijing"); address.Street.ShouldEqual("Dongzhimen Street"); address.PostCode.ShouldEqual("100001");
如果AddressDto中有值為空的屬性,AutoMapper在映射的時候會把Address中的相應屬性也置為空:
Address address = Mapper.Map<AddressDto,Address>(new AddressDto { Country = "China" }); address.City.ShouldBeNull(); address.Street.ShouldBeNull(); address.PostCode.ShouldBeNull();
甚至如果傳入一個空的AddressDto,AutoMapper也會幫我們得到一個空的Address對象。
Address address = Mapper.Map<AddressDto,Address>(null); address.ShouldBeNull();
千萬不要把這種Convention的映射方式當成“玩具”,它在映射具有相同字段名的復雜類型的時候還是具有相當大的威力的。
例如,考慮我們的BookStoreDto到BookStore的映射,兩者的字段名稱完全相同,只是字段的類型不一致。如果我們定義好了BookDto到Book的映射規則,再加上上述Convention方式的AddressDto到Address的映射,就可以用“零配置”實現BookStoreDto到BookStore的映射了:
IMappingExpression<BookDto, Book> expression = Mapper.CreateMap<BookDto,Book>(); // Define mapping rules from BookDto to Book here Mapper.CreateMap<AddressDto, Address>(); Mapper.CreateMap<BookStoreDto, BookStore>();
然后我們就可以直接轉換BookStoreDto了:
BookStoreDto dto = new BookStoreDto { Name = "My Store", Address = new AddressDto { City = "Beijing" }, Books = new List<BookDto> { new BookDto {Title = "RESTful Web Service"}, new BookDto {Title = "Ruby for Rails"}, } }; BookStore bookStore = Mapper.Map<BookStoreDto,BookStore>(dto); bookStore.Name.ShouldEqual("My Store"); bookStore.Address.City.ShouldEqual("Beijing"); bookStore.Books.Count.ShouldEqual(2); bookStore.Books.First().Title.ShouldEqual("RESTful Web Service"); bookStore.Books.Last().Title.ShouldEqual("Ruby for Rails");
實現BookDto到Book之間的轉換(他嵌套了相應的子類型如:Publisher ->ContactInfo,Author):
var exp = Mapper.CreateMap<BookDto, Book>(); exp.ForMember(bok=> bok.Publisher/*(變量)*/, (map) => map.MapFrom(dto=>new Publisher(){Name= dto.Publisher/*(DTO的變量)*/}));
一般在我們寫完規則之后通常會調用,該方法主要用來檢查還有那些規則沒有寫完。
Mapper.AssertConfigurationIsValid();
參見:http://stackoverflow.com/questions/4928487/how-to-automap-thismapping-sub-members
其它的就以此類推。
如果要完成 BookStore 到 BookStoreDto 具體的應該如何映射呢?
相同的類型與名字就不說了,如BookStore.Name->BookStoreDto.Name AutoMapper會自動去找。而對於List<Book>與List<BookDto>者我們必須在配置下面代碼之前
var exp = Mapper.CreateMap<BookStore, BookStoreDto>(); exp.ForMember(dto => dto.Books, (map) => map.MapFrom(m => m.Books));
告訴AutoMapper,Book與BookDto的映射,最后效果為:
Mapper.CreateMap<Book, BookDto>(); var exp = Mapper.CreateMap<BookStore, BookStoreDto>(); exp.ForMember(dto => dto.Books, (map) => map.MapFrom(m => m.Books));
Address同理。如果要完成不同類型之間的轉換用AutoMapper,如string到int,string->DateTime,以及A->B之間的類型轉換我們可以參照如下例子:
http://automapper.codeplex.com/wikipage?title=Custom%20Type%20Converters&referringTitle=Home
對於我們不想要某屬性有值我們可以采用下面的方式。
exp.ForMember(ads => ads.ZipCode, dto => dto.Ignore()); //如果對於不想某屬性有值,我們可以通過Ignore來忽略他,這樣在調用AssertConfigurationIsValid時也不會報錯.
三,定義類型間的簡單映射規則
前面我們看了Convention的映射方式,客觀的說還是有很多類型間的映射是無法通過簡單的Convention方式來做的,這時候就需要我們使用Configuration了。好在我們的Configuration是在代碼中以“強類型”的方式來寫的,比寫繁瑣易錯的xml方式是要好的多了。 先來看看BookDto到Publisher的映射。 回顧一下前文中定義的規則:BookDto.Publisher -> Publisher.Name。 在AutoMapperzhong,我們可以這樣映射:
var map = Mapper.CreateMap<BookDto,Publisher>(); map.ForMember(d => d.Name, opt => opt.MapFrom(s => s.Publisher));
AutoMapper使用ForMember來指定每一個字段的映射規則:
還好有強大的lambda表達式,規則的定義簡單明了。 此外,我們還可以使用ConstructUsing的方式一次直接定義好所有字段的映射規則。例如我們要定義BookDto到第一作者(Author)的ContactInfo的映射,使用ConstructUsing方式,我們可以:
var map = Mapper.CreateMap<BookDto,ContactInfo>(); map.ConstructUsing(s => new ContactInfo { Blog = s.FirstAuthorBlog, Email = s.FirstAuthorEmail, Twitter = s.FirstAuthorTwitter });
然后,就可以按照我們熟悉的方式來使用了:
BookDto dto = new BookDto { FirstAuthorEmail = "matt.rogen@abc.com", FirstAuthorBlog = "matt.amazon.com", }; ContactInfo contactInfo = Mapper.Map<BookDto, ContactInfo>(dto);
如果需要映射的2個類型有部分字段名稱相同,又有部分字段名稱不同呢?還好AutoMapper給我們提供的Convention或Configuration方式並不是“異或的”,我們可以結合使用兩種方式,為名稱不同的字段配置映射規則,而對於名稱相同的字段則忽略配置。
例如:對於前面提到的AddressDto到Address的映射,假如AddressDto的字段Country不叫Country叫CountryName,那么在寫AddressDto到Address的映射規則時,只需要:
var map = Mapper.CreateMap<AddressDto, Address>(); map.ForMember(d => d.Country, opt => opt.MapFrom(s => s.CountryName));
對於City、Street和PostCode無需定義任何規則,AutoMapper仍然可以幫我們進行正確的映射。