使用AutoMapper实现Dto和Model的自由转换(下)
【四】将一个类型映射为类型体系
先回顾一下我们的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真是奇慢无比,所有我决定先把自己的代码打包上传,欢迎大家参考使用。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。