一些orm框架,在用到Entity的時候有一些開源代碼用到了automapper(如:nopcommence),將數據對象轉成DTO。好比在ORM中,與數據庫交互用的Model模型是具備不少屬性變量方法神馬的。而當咱們與其它系統(或系統中的其它結構)進行數據交互時,出於耦合性考慮或者安全性考慮或者性能考慮(總之就是各類考慮),咱們不但願直接將這個Model模型傳遞給它們,這時咱們會建立一個貧血模型來保存數據並傳遞。神馬是貧血模型?貧血模型(DTO,Data Transfer Object)就是說只包含屬性神馬的,只能保存必須的數據,木有其它任何的多餘的方法數據什麼的,專門用於數據傳遞用的類型對象。在這個建立的過程當中,若是咱們手動來進行,就會看到這樣的代碼:
B b=new B();
b.XXX1=a.XXX1;
b.XXX2=a.XXX2;
...
...
...
return b;
此時,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): express
- 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): app
- 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: post
- 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仍然能夠幫咱們進行正確的映射。