代码改变世界

AutoMapper小结

2014-01-03 16:58  呆河马  阅读(41227)  评论(22编辑  收藏
一些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 uses a convention-based matching algorithm to match up source to destination values.

我们要做的只是将要映射的两个类型告诉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来指定每一个字段的映射规则: 

引用:The each custom member configuration uses an action delegate to configure each member.

还好有强大的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仍然可以帮我们进行正确的映射。