在实际的软件开发项目中,我们的“业务逻辑”常常需要我们对同样的数据进行各种变换。
例如,一个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 = "[email protected]",
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仍然可以帮我们进行正确的映射。