書接上文。在上一篇文章中我們討論了使用AutoMapper實作類別型間1-1映射的兩種方式——Convention和Configuration,知道了如何進行簡單的OO Mapping。在這個系列的最後一篇文章我想基於我們的需求討論一些中層級的話題,包括:如何?類型體型之間的映射,以及如何為兩個類型實現多個映射規則。
【四】將一個類型映射為類型體系
先回顧一下我們的Dto和Model。我們有BookDto,我們有Author,每個Author有自己的ContactInfo。現在提一個問題:如何從BookDto得到第一個作者的Author對象呢?答案即簡單,又不簡單。
最簡單的做法是,使用前面提到的CountructUsing,指定BookDto到Author的全部欄位及子類型欄位的映射:
C#代碼
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為實現欄位層級的特定映射邏輯而定義的類型,它的定義如下:
C#代碼
public interface IValueResolver
{
ResolutionResult Resolve(ResolutionResult source);
}
而在實際的應用中我們往往會使用它的泛型子類——ValueResolver,並實現它的抽象方法:
C#代碼
protected abstract TDestination ResolveCore(TSource source);
其中TSource為源類型,TDestination為目標欄位的類型。
回到我們的例子,我們現在可以這樣來映射BookDto -> Author:
C#代碼
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的邏輯:
C#代碼
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方法應該這樣定義:
C#代碼
CreateMap<TSource, TDestination>(string tag)
Map<TSource, TDestination>(TSource, string tag)
其中有一個額外的參數tag用於標識該映射的標籤。
而我們在使用的時候,就可以:
C#代碼
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:
C#代碼
public class MappingEngineProvider
{
private readonly MappingEngine _engine;
public MappingEngine Get()
{
return _engine;
}
}
我們將不同類型的映射規則抽象為介面IMapping:
C#代碼
public interface IMapping
{
void AddTo(Configuration config);
}
然後在MappingEngineProvider的建構函式裡將需要的規則放到對應的MappingEngine中:
C#代碼
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:
C#代碼
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裡:
C#代碼
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對象:
C#代碼
public static SimpleMappingEngineProvider First = new MappingEngineProvider(Engine.First);
public static SimpleMappingEngineProvider Second = new MappingEngineProvider(Engine.Second);
現在我們就可以在映射BookDto -> Book的時候同時使用這2個Engine來得到2個Author並把它們組裝到欄位Book.Authors裡面了:
C#代碼
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沒有幫我們實現,就讓我們自己來實現吧:
C#代碼
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);
}
}
一切又都回來了,我們可以這樣:
C#代碼
var firstAuthor = MyMapper.Map<BookDto,Author>(dto, Engine.First);
var secondAuthor = MyMapper.Map<BookDto,Author>(dto, Engine.Second);
也可以這樣了:
C#代碼
var book = MyMapper.Map<BookDto,book>(dto);
後記: 發現在家裡要上傳檔案到Github真是奇慢無比,所有我決定先把自己的代碼打包上傳,歡迎大家參考使用。