AutoMapper是一个.NET的对象映射工具。
项目地址:https://github.com/AutoMapper/AutoMapper。
帮助文档:https://github.com/AutoMapper/AutoMapper/wiki
主要用途
领域对象与DTO之间的转换、数据库查询结果映射至实体对象。
使用笔记
场景1:源类型BlogEntry,目标类型BlogPostDto,指定属性进行映射(BlogEntry.ID对应于BlogPostDto.PostId)。
代码:
AutoMapper.Mapper.CreateMap<BlogEntry, BlogPostDto>() .ForMember(dto => dto.PostId, opt => opt.MapFrom(entity => entity.ID));
场景2:IDataReader映射至实体类
代码:
using (IDataReader reader = _db.ExecuteReader(command)) { if (reader.Read()) { return AutoMapper.Mapper.DynamicMap<BlogConfig>(reader); } }
场景3:列表类型之间的映射,比如:源类型List<BlogSite>,目标类型List<BlogSiteDto>
代码如下:
AutoMapper.Mapper.CreateMap<BlogSite, BlogSiteDto>();
var blogSiteDto = AutoMapper.Mapper.Map<List<BlogSite>, List<BlogSiteDto>>(blogSite);
注:必须要先通过CreateMap建立BlogSite与BlogSiteDto的映射关系。
场景4:在映射时为目标实例的属性指定值
代码如下:
var blogSiteDto = new BlogSiteDto(); AutoMapper.Mapper.CreateMap<BlogEntry, BlogPostDto>() .ForMember(dto => dto.BlogSiteDto, opt => opt.UseValue(blogSiteDto));
注:BlogSiteDto是BlogPostDto的一个属性。
补充:
AutoMapper的配置(比如AutoMapper.Mapper.CreateMap<BlogSite, BlogSiteDto>();)建议放在程序启动时,比如Global.asax的Application_Start, BootStrapper。
在实际的软件开发项目中,我们的“业务逻辑”常常需要我们对同样的数据进行各种变换。例如,一个Web应用通过前端收集用户的输入成为Dto,然后将Dto转换成领域模型并持久化到数据库中。另一方面,当用户请求数据时,我们又需要做相反的工作:将从数据库中查询出来的领域模型以相反的方式转换成Dto再呈现给用户。有时候我们还会面临更多的数据使用需求,例如有多个数据使用的客户端,每个客户端都有自己对数据结构的不同需求,而这也需要我们进行更多的数据转换。
频繁的数据转换琐碎而又凌乱,很多时候我们不得不:
(1)在两个类型几乎只是名字不同而结构大体相似,却只能以手工的、逐个属性赋值的方式实现数据在类型间的“传递”。
(2)每遇到一个新的数据转换场景就手动实现一套转换逻辑,导致数据转换操作重复而又分散到应用的各个角落。
如果有这样一个“变形金刚”般的工具,把“橘子”变成我们想要的“苹果”,而我们需要做的只是定义好转换规则——做我们真正的业务逻辑,或者甚至在简单场景下连规则都不需要定义(Convention Over Configuration),那将会是非常美好的事情。事实上在.NET中我们不用重复发明轮子,因为我们有——AutoMapper,一个强大的Object-Object Mapping工具。
好吧,我承认自己有一点小小的激动,事实上我所做的项目正在经历以上的“困惑”,而AutoMapper确实带给我眼前一亮的感觉。因此我花了一点周末休息时间小小尝试了一把AutoMapper,通过做小的应用场景实现Dto到领域模型的映射,确实感觉到了它的“强大气场”。我将在文章中分享自己的使用心得,希望能给同样处于困惑中的你带来一点帮助。完整的项目代码我会在晚一些时候发布到自己的git repository中,欢迎大家自由参考使用。
【一】 应用场景说明
先来看看我所”虚拟“的领域模型。这一次我定义了一个书店(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之间的数据转换。
在上一篇文章中我们构造出了完整的应用场景,包括我们的Model、Dto以及它们之间的转换规则。下面就可以卷起袖子,开始我们的AutoMapper之旅了。
【二】以Convention方式实现零配置的对象映射
我们的AddressDto和Address结构完全一致,且字段名也完全相同。对于这样的类型转换,AutoMapper为我们提供了Convention,正如它的官网上所说的:
我们要做的只是将要映射的两个类型告诉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");
【三】定义类型间的简单映射规则
前面我们看了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 = "[email protected]", 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仍然可以帮我们进行正确的映射。
书接上文。在上一篇文章中我们讨论了使用AutoMapper实现类型间1-1映射的两种方式——Convention和Configuration,知道了如何进行简单的OO Mapping。在这个系列的最后一篇文章我想基于我们的需求讨论一些中级别的话题,包括:如何实现类型体型之间的映射,以及如何为两个类型实现多个映射规则。
【四】将一个类型映射为类型体系
先回顾一下我们的Dto和Model。我们有BookDto,我们有Author,每个Author有自己的ContactInfo。现在提一个问题:如何从BookDto得到第一个作者的Author对象呢?答案即简单,又不简单。
最简单的做法是,使用前面提到的CountructUsing,指定BookDto到Author的全部字段及子类型字段的映射:
var map = Mapper.CreateMap<BookDto,Author>(); map.ConstructUsing(s => new Author { Name = s.FirstAuthorName, Description = s.FirstAuthorDescription, ContactInfo = new ContactInfo { Blog = s.FirstAuthorBlog, Email = s.FirstAuthorEmail, Twitter = s.FirstAuthorTwitter } });
这样的做法可以工作,但很不经济。因为我们是在从头做BookDto到Author的映射,而从BookDto到ContactInfo的映射是我们之前已经实现过的,实在没有必要重复再写一遍。设想一下,如果有一个别的什么Reader类型里面也包含有ContactInfo,在做BookDto到Reader映射的时候,我们是不是再写一遍这个BookDto -> ContactInfo逻辑呢?再设想一下如果我们在实现BookDto到Book的映射的时候,是不是又需要把BookDto到Author的映射规则再重复写一遍呢?
所以我认为对于这种类型体系间的映射,比较理想的做法是为每个具体类型指定简单的映射,而后在映射复杂类型的时候再复用简单类型的映射。用简单点的语言描述:
我们有A,B,C,D四个类型,其中B = [C, D]。已知A -> C, A -> D, 求A -> B。
我的解法是使用AutoMapper提供的——IValueResolver。IValueResolver是AutoMapper为实现字段级别的特定映射逻辑而定义的类型,它的定义如下:
public interface IValueResolver { ResolutionResult Resolve(ResolutionResult source); }
而在实际的应用中我们往往会使用它的泛型子类——ValueResolver,并实现它的抽象方法:
protected abstract TDestination ResolveCore(TSource source);
其中TSource为源类型,TDestination为目标字段的类型。
回到我们的例子,我们现在可以这样来映射BookDto -> Author:
var map = Mapper.CreateMap<BookDto, Author>(); map.ForMember(d => d.Name, opt => opt.MapFrom(s => s.FirstAuthorName)) .ForMember(d => d.Description, opt => opt.MapFrom(s => s.FirstAuthorDescription)) .ForMember(d => d.ContactInfo, opt => opt.ResolveUsing<FirstAuthorContactInfoResolver>()));
在FirstAuthorContactInfoResolver中我们实现ValueResolver并复用BookDto -> ContactInfo的逻辑:
public class FirstAuthorContactInfoResolver : ValueResolver<BookDto,ContactInfo> { protected override ContactInfo ResolveCore(BookDto source) { return Mapper.Map<BookDto, ContactInfo>(source); } }
一切就搞定了。
类似的,我们现在也可以实现BookDto -> Book了吧?通过复用BookDto -> Author以及BookDto -> Publisher。
真的可以吗?好像还有问题。是的,我们会发现需要从BookDto映射到两个不同的Author,它们的字段映射规则是不同的。怎么办?赶紧进入我们的最后一个议题。
【五】为两个类型实现多套映射规则
我们的问题是:对于类型A和B,需要定义2个不同的A -> B,并让它们可以同时使用。事实上目前的AutoMapper并没有提供现成的方式做到这一点。
当然我们可以采用“曲线救国”的办法——为first author和second author分别定义Author的两个子类,比如说FirstAuthor和SecondAuthor,然后分别实现BookDto -> FirstAuthor和BookDto -> SecondAuthor映射。但是这种方法也不太经济。假如还有第三作者甚至第四作者呢?为每一个作者都定义一个Author的子类吗?
另一方面,我们不妨假设一下,如果AutoMapper提供了这样的功能,那会是什么样子呢?CreateMap方法和Map方法应该这样定义:
CreateMap<TSource, TDestination>(string tag) Map<TSource, TDestination>(TSource, string tag)
其中有一个额外的参数tag用于标识该映射的标签。
而我们在使用的时候,就可以:
var firstAuthorMap = Mapper.CreateMap<BookDto, Author>("first"); // Define BookDto -> first Author rule var secondAuthorMap = Mapper.CreateMap<BookDto, Author>("second"); // Define BookDto -> second Author rule var firstAuthor = Mapper.Map<BookDto, Author>(source, "first"); var secondAuthor = Mapper.Map<BookDto, Author>(source, "second");
遗憾的是,这一切都是假如。但是没有关系,虽然AutoMapper关上了这扇门,却为我们留着另一扇门——MappingEngine。
MappingEngine是AutoMapper的映射执行引擎,事实上在Mapper中有默认的MappingEngine,我们在调用Mapper.CreateMap的时候,是往与这个默认的MappingEngine对应的Configuration中写规则,在调用Mapper.Map获取对象的时候则是使用默认的MappingEngine执行其对应Configuration中的规则。
简而言之一个MappingEngine就是一个AutoMapper的“虚拟机”,如果我们同时启动多个“虚拟机”,并且将针对同一对类型的不同映射规则放到不同的“虚拟机”上,就可以让它们各自相安无事的运行起来,使用的时候要用哪个规则就问相应的“虚拟机”去要好了。
说做就做。首先我们定义一个MappingEngineProvider类,用它来获取不同的MappingEngine:
public class MappingEngineProvider { private readonly MappingEngine _engine; public MappingEngine Get() { return _engine; } }
我们将不同类型的映射规则抽象为接口IMapping:
public interface IMapping { void AddTo(Configuration config); }
然后在MappingEngineProvider的构造函数里将需要的规则放到对应的MappingEngine中:
private static Dictionary<Engine,List<IMapping>> _rules=new Dictionary<Engine, List<IMapping>>(); public MappingEngineProvider(Engine engine) { var config = new Configuration(new TypeMapFactory(), MapperRegistry.AllMappers()); _rules[engine].ForEach(r=> r.AddTo(config)); _mappingEngine = new MappingEngine(config); }
注意到这里我们用了一个枚举类型Engine用于标识可能的MappingEngine:
public enum Engine { Basic = 0, First, Second }
我们用到了3个Engine,Basic用于放置所有基本的映射规则,First用于放置所有Dto -> FirstXXX的规则,Second则用于放置所有Dto -> SecondXXX的规则。
我们还定义了一个放置所有映射规则的字典_rule,将规则分门别类放到不同的Engine中。
剩下的事情就是往字典_rule里填充我们的mapping了。比如说我们把BookDtoToFirstAuthorMapping放到First engine里并把BookDtoToSecondAuthorMapping放到Second engine里:
private static readonly Dictionary<Engine, List<IMapping>> _rules = new Dictionary<Engine, List<IMapping>> { { Engine.First, new List<IMapping> { new BookDtoToFirstAuthorMapping(), } }, { Engine.Second, new List<IMapping> { new BookDtoToSecondAuthorMapping(), } }, };
当然为了方便使用我们可以事先实例化好不同的MappingEngineProvider对象:
public static SimpleMappingEngineProvider First = new MappingEngineProvider(Engine.First); public static SimpleMappingEngineProvider Second = new MappingEngineProvider(Engine.Second);
现在我们就可以在映射BookDto -> Book的时候同时使用这2个Engine来得到2个Author并把它们组装到字段Book.Authors里面了:
public class BookDtoToBookMapping : DefaultMapping<BookDto, Book> { protected override void MapMembers(IMappingExpression<BookDto, Book> map) { map.ForMember(d => d.Authors, opt => opt.ResolveUsing<AuthorsValueResolver>()); } private class AuthorsValueResolver : ValueResolver<BookDto, List<Author>> { protected override List<Author> ResolveCore(BookDto source) { var firstAuthor = SimpleMappingEngineProvider.First.Get().Map<BookDto, Author>(source); var secondAuthor = SimpleMappingEngineProvider.Second.Get().Map<BookDto, Author>(source); return firstAuthor.IsNull() ? secondAuthor.IsNull() ? new List<Author>() : new List<Author> {new Author(), secondAuthor} : secondAuthor.IsNull() ? new List<Author> {firstAuthor} : new List<Author> {firstAuthor, secondAuthor}; } } }
最后,还记得我们在本节开始的时候提到的美好愿望吗?既然AutoMapper没有帮我们实现,就让我们自己来实现吧:
public class MyMapper { private static readonly Dictionary<Engine, MappingEngine> Engines = new Dictionary<Engine, MappingEngine> { {Engine.Basic, MappingEngineProvider.Basic.Get()}, {Engine.First, MappingEngineProvider.First.Get()}, {Engine.Second, MappingEngineProvider.Second.Get()}, }; public static TTarget Map<TSource, TTarget>(TSource source, Engine engine = Engine.Basic) { return Engines[engine].Map<TSource, TTarget>(source); } }
一切又都回来了,我们可以这样:
var firstAuthor = MyMapper.Map<BookDto,Author>(dto, Engine.First); var secondAuthor = MyMapper.Map<BookDto,Author>(dto, Engine.Second);
也可以这样了:
var book = MyMapper.Map<BookDto,book>(dto);
后记: 发现在家里要上传文件到Github真是奇慢无比,所有我决定先把自己的代码打包上传,欢迎大家参考使用。