在實際的軟件開發項目中,我們的“業務邏輯”常常需要我們對同樣的數據進行各種變換。
例如,一個Web應用通過前端收集用戶的輸入成為Dto,然后將Dto轉換成領域模型並持久化到數據庫中。相反,當用戶請求數據時,我們又需要做相反的工作:將從數據庫中查詢出來的領域模型以相反的方式轉換成Dto再呈現給用戶。
有時候我們還會面臨更多的數據使用需求,例如有多個數據使用的客戶端,每個客戶端都有自己對數據結構的不同需求,而這也需要我們進行更多的數據轉換。
頻繁的數據轉換瑣碎而又凌亂,很多時候我們不得不做:
(1)在兩個類型幾乎只是名字不同而結構大體相似,卻只能以手工的、逐個屬性賦值的方式實現數據在類型間的“傳遞”。
(2)每遇到一個新的數據轉換場景就手動實現一套轉換邏輯,導致數據轉換操作重復而又分散到應用的各個角落。
如果有這樣一個“變形金剛”般的工具,把“橘子”變成我們想要的“蘋果”,而我們需要做的只是定義好轉換規則——做我們真正的業務邏輯,或者甚至在簡單場景下連規則都不需要定義(Convention Over Configuration),那將會是非常美好的事情。事實上在.NET中我們不用重復發明輪子,因為我們有——AutoMapper,一個強大的Object-Object Mapping工具。
好吧,我承認自己有一點小小的激動,事實上我所做的項目正在經歷以上的“困惑”,而AutoMapper確實帶給我眼前一亮的感覺。因此我花了一點周末休息時間小小嘗試了一把AutoMapper,通過做小的應用場景實現Dto到領域模型的映射,確實感覺到了它的“強大氣場”。我將在文章中分享自己的使用心得,希望能給同樣處於困惑中的你帶來一點幫助。完整的項目代碼我會在晚一些時候發布到自己的git repository中,歡迎大家自由參考使用。
【一】 將Model轉換為Dto
先來看看我所”虛擬“的領域模型。這一次我定義了一個書店(BookStore):
1: public class BookStore
2: {
3: public string Name { get; set; }
4: public List<Book> Books { get; set; }
5: public Address Address { get; set; }
6: }
書店有自己的地址(Address):
1: public class Address
2: {
3: public string Country { get; set; }
4: public string City { get; set; }
5: public string Street { get; set; }
6: public string PostCode { get; set; }
7: }
同時書店里放了n本書(Book):
1: public class Book
2: {
3: public string Title { get; set; }
4: public string Description { get; set; }
5: public string Language { get; set; }
6: public decimal Price { get; set; }
7: public List<Author> Authors { get; set; }
8: public DateTime? PublishDate { get; set; }
9: public Publisher Publisher { get; set; }
10: public int? Paperback { get; set; }
11: }
每本書都有出版商信息(Publisher):
1: public class Publisher
2: {
3: public string Name { get; set; }
4: }
每本書可以有最多2個作者的信息(Author):
1: public class Author
2: {
3: public string Name { get; set; }
4: public string Description { get; set; }
5: public ContactInfo ContactInfo { get; set; }
6: }
每個作者都有自己的聯系方式(ContactInfo):
1: public class ContactInfo
2: {
3: public string Email { get; set; }
4: public string Blog { get; set; }
5: public string Twitter { get; set; }
6: }
差不多就是這樣了,一個有着層級結構的領域模型。
再來看看我們的Dto結構。
在Dto中我們有與BookStore對應的BookStoreDto:
1: public class BookStoreDto
2: {
3: public string Name { get; set; }
4: public List<BookDto> Books { get; set; }
5: public AddressDto Address { get; set; }
6: }
其中包含與Address對應的AddressDto:
1: public class AddressDto
2: {
3: public string Country { get; set; }
4: public string City { get; set; }
5: public string Street { get; set; }
6: public string PostCode { get; set; }
7: }
以及與Book相對應的BookDto:
1: public class BookDto
2: {
3: public string Title { get; set; }
4: public string Description { get; set; }
5: public string Language { get; set; }
6: public decimal Price { get; set; }
7: public DateTime? PublishDate { get; set; }
8: public string Publisher { get; set; }
9: public int? Paperback { get; set; }
10: public string FirstAuthorName { get; set; }
11: public string FirstAuthorDescription { get; set; }
12: public string FirstAuthorEmail { get; set; }
13: public string FirstAuthorBlog { get; set; }
14: public string FirstAuthorTwitter { get; set; }
15: public string SecondAuthorName { get; set; }
16: public string SecondAuthorDescription { get; set; }
17: public string SecondAuthorEmail { get; set; }
18: public string SecondAuthorBlog { get; set; }
19: public string SecondAuthorTwitter { get; set; }
20: }
注意到我們的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之間的數據轉換。
【二】將Dto轉換為Model
1,以Convention方式實現零配置的對象映射
我們的AddressDto和Address結構完全一致,且字段名也完全相同。對於這樣的類型轉換,AutoMapper為我們提供了Convention,正如它的官網上所說的:
引用
AutoMapper uses a convention-based matching algorithm to match up source to destination values.
我們要做的只是將要映射的兩個類型告訴AutoMapper(調用Mapper類的Static方法CreateMap並傳入要映射的類型):
C#代碼
Mapper.CreateMap<AddressDto, Address>();
然后就可以交給AutoMapper幫我們搞定一切了:
1: AddressDto dto = new AddressDto
2: {
3: Country = "China",
4: City = "Beijing",
5: Street = "Dongzhimen Street",
6: PostCode = "100001"
7: };
8: Address address = Mapper.Map<AddressDto,Address>(Dto);
9: address.Country.ShouldEqual("China");
10: address.City.ShouldEqual("Beijing");
11: address.Street.ShouldEqual("Dongzhimen Street");
12: address.PostCode.ShouldEqual("100001");
如果AddressDto中有值為空的屬性,AutoMapper在映射的時候會把Address中的相應屬性也置為空:
1: Address address = Mapper.Map<AddressDto,Address>(new AddressDto
2: {
3: Country = "China"
4: });
5: address.City.ShouldBeNull();
6: address.Street.ShouldBeNull();
7: address.PostCode.ShouldBeNull();
甚至如果傳入一個空的AddressDto,AutoMapper也會幫我們得到一個空的Address對象。
1: Address address = Mapper.Map<AddressDto,Address>(null);
2: address.ShouldBeNull();
千萬不要把這種Convention的映射方式當成“玩具”,它在映射具有相同字段名的復雜類型的時候還是具有相當大的威力的。
例如,考慮我們的BookStoreDto到BookStore的映射,兩者的字段名稱完全相同,只是字段的類型不一致。如果我們定義好了BookDto到Book的映射規則,再加上上述Convention方式的AddressDto到Address的映射,就可以用“零配置”實現BookStoreDto到BookStore的映射了:
C#代碼
1: IMappingExpression<BookDto, Book> expression = Mapper.CreateMap<BookDto,Book>();
2: // Define mapping rules from BookDto to Book here
3: Mapper.CreateMap<AddressDto, Address>();
4: Mapper.CreateMap<BookStoreDto, BookStore>();
然后我們就可以直接轉換BookStoreDto了:
1: BookStoreDto dto = new BookStoreDto
2: {
3: Name = "My Store",
4: Address = new AddressDto
5: {
6: City = "Beijing"
7: },
8: Books = new List<BookDto>
9: {
10: new BookDto {Title = "RESTful Web Service"},
11: new BookDto {Title = "Ruby for Rails"},
12: }
13: };
14: BookStore bookStore = Mapper.Map<BookStoreDto,BookStore>(dto);
15: bookStore.Name.ShouldEqual("My Store");
16: bookStore.Address.City.ShouldEqual("Beijing");
17: bookStore.Books.Count.ShouldEqual(2);
18: bookStore.Books.First().Title.ShouldEqual("RESTful Web Service");
19: bookStore.Books.Last().Title.ShouldEqual("Ruby for Rails");
以下為自己補充:
(Begin)---------------------------------------------------------------
1, 要實現BookDto到Book之間的轉換還是有一斷路需要走的因為他嵌套了相應的子類型如:Publisher ->ContactInfo,
Author。廢話少說,直接上答案:
1: var exp = Mapper.CreateMap<BookDto, Book>();
2: exp.ForMember(bok=> bok.Publisher/*(變量)*/,
3: (map) => map.MapFrom(dto=>new Publisher(){Name= dto.Publisher/*(DTO的變量)*/}));
一般在我們寫完規則之后通常會調用
//該方法主要用來檢查還有那些規則沒有寫完。 Mapper.AssertConfigurationIsValid();
參見:http://stackoverflow.com/questions/4928487/how-to-automap-thismapping-sub-members
其它的就以此類推。
2,如果要完成 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同理。
3,如果要完成不同類型之間的轉換用AutoMapper,如string到int,string->DateTime,以及A->B之間的類型轉換我們可以參照如下例子:
http://automapper.codeplex.com/wikipage?title=Custom%20Type%20Converters&referringTitle=Home
4, 對於我們不想要某屬性有值我們可以采用下面的方式。
exp.ForMember(ads => ads.ZipCode, dto => dto.Ignore()); //如果對於不想某屬性有值,我們可以通過Ignore來忽略他,這樣在調用AssertConfigurationIsValid時也不會報錯.
(End)------------------------------------------------------------------------------------
【三】定義類型間的簡單映射規則
前面我們看了Convention的映射方式,客觀的說還是有很多類型間的映射是無法通過簡單的Convention方式來做的,這時候就需要我們使用Configuration了。好在我們的Configuration是在代碼中以“強類型”的方式來寫的,比寫繁瑣易錯的xml方式是要好的多了。
先來看看BookDto到Publisher的映射。
回顧一下前面中定義的規則:BookDto.Publisher -> Publisher.Name。
在AutoMapperzhong,我們可以這樣映射:
1: var map = Mapper.CreateMap<BookDto,Publisher>();
2: map.ForMember(d => d.Name, opt => opt.MapFrom(s => s.Publisher));
AutoMapper使用ForMember來指定每一個字段的映射規則:
引用
The each custom member configuration uses an action delegate to configure each member.
還好有強大的lambda表達式,規則的定義簡單明了。
此外,我們還可以使用ConstructUsing的方式一次直接定義好所有字段的映射規則。例如我們要定義BookDto到第一作者(Author)的ContactInfo的映射,使用ConstructUsing方式,我們可以:
C#代碼
1: var map = Mapper.CreateMap<BookDto,ContactInfo>();
2: map.ConstructUsing(s => new ContactInfo
3: {
4: Blog = s.FirstAuthorBlog,
5: Email = s.FirstAuthorEmail,
6: Twitter = s.FirstAuthorTwitter
7: });
然后,就可以按照我們熟悉的方式來使用了:
1: BookDto dto = new BookDto
2: {
3: FirstAuthorEmail = "matt.rogen@abc.com",
4: FirstAuthorBlog = "matt.amazon.com",
5: };
6: ContactInfo contactInfo = Mapper.Map<BookDto, ContactInfo>(dto);
如果需要映射的2個類型有部分字段名稱相同,又有部分字段名稱不同呢?還好AutoMapper給我們提供的Convention或Configuration方式並不是“異或的”,我們可以結合使用兩種方式,為名稱不同的字段配置映射規則,而對於名稱相同的字段則忽略配置。
例如對於前面提到的AddressDto到Address的映射,假如AddressDto的字段Country不叫Country叫CountryName,那么在寫AddressDto到Address的映射規則時,只需要:
1: var map = Mapper.CreateMap<AddressDto, Address>();
2: map.ForMember(d => d.Country, opt => opt.MapFrom(s => s.CountryName));
對於City、Street和PostCode無需定義任何規則,AutoMapper仍然可以幫我們進行正確的映射。
該文轉自:http://zz8ss5ww6.iteye.com/blog/1126219