在上一篇我們介紹了AutoMapper的設定和用法,使用起來肯定比自己手動做左邊倒到右邊還要簡單。
不過AutoMapper也不是沒有它自己的問題,最麻煩的地方在於設定Entity和Class之間的對應。這一篇要探討的就是,如何透過框架來減少這方面的設定。
框架思路
我們先來思考一下我們會如何達到簡化設定對應邏輯,然後在開始開發。
首先,其實AutoMapper本身有所謂的Profile,可以透過Profile來設定Entity和ViewModel之間的對應。不過我個人比較傾向於Entity和ViewModel的對應邏輯是能夠簡單看到並且是在一起,換句話說,如果能夠在ViewModel定義好和Entity的對應關係不是很好,因為只要一找到ViewModel,馬上就知道它和Entity的關係。
有了這個概念,我們就可以來看一下我們如何透過Interface來達到這個效果。
Interface的定義
我們要提供兩種定義的方式:
IMapFrom<T>
- T表示這個ViewModel對應的EntityIHaveCustomMapping
- 表示這個ViewModel要自己對應Entity和設定自己的邏輯
因此看起來會是:
然後實際的C#程式碼是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /// <summary> /// 設定ViewModel要對應的Model。 /// 這個用預設的Convention來對應 /// </summary> /// <typeparam name="T">要被對應到的Type</typeparam> public interface IMapFrom<T> { } /// <summary> /// 設定ViewModel要對應的Model /// 如果需要客制AutoMapper的邏輯,讓ViewModel實作此Interface /// </summary> public interface IHaveCustomMapping { /// <summary> /// 設定自定義的Mapping邏輯 /// </summary> /// <param name="configuration">Automapper的Config物件</param> void CreateMappings(IConfiguration configuration); } |
使用兩個interface的差異
使用IMapFrom<T>
我們先看一下上一篇我們IndexViewModel
本來的用法:
1 2 3 | Mapper.CreateMap<Post, IndexViewModel>(); var projectIQueryable = (db.Post.Project().To<IndexViewModel>()).ToList(); |
如果改用成我們的interface,會變成:
1 2 3 4 5 6 7 8 | // ViewModel加上interface public class IndexViewModel : IMapFrom<Post> .... // 在實際呼叫的時候,會和之前一樣,只是不需要呼叫CreatMap var projectIQueryable = (db.Post.Project().To<IndexViewModel>()).ToList(); |
使用IHaveCustomMapping
這個是假設有特殊的對應邏輯才在呼叫,使用上會是:
1 2 3 4 5 6 7 8 9 10 | public class IndexViewModel : IHaveCustomMapping { // properties public void CreateMappings(IConfiguration configuration) { configuration.CreateMap<Post, IndexViewModel>(); } } |
可以看到,AutoMapper的IConfiguration
會被傳進來,這時候就可以手動設定對應邏輯。
到這邊為止,我們interface的定義和使用就完成了,不過接下來我們還需要讓這兩個interface實際有作用,要不然是沒有效果。
在系統啟動的時候註冊AutoMapper對應
當我們用了interface把這些ViewModel的對應都定義好了之後,我們希望在系統啟動了之後,讀出所有設定過這兩種interface的ViewModel,並且作出對應的AutoMapper設定。
我們首先寫好使用這兩個interface的邏輯:
顯示取得所有實作這兩個interface的type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /// <summary> /// 註冊有設定AutoMapper的viewmodel /// </summary> public class AutoMapperConfig : IRunAtStartup { /// <summary> /// 要執行的邏輯 /// </summary> public void Execute() { var typeOfIHaveCustomMapping = typeof (IHaveCustomMapping); var typeOfIMapFrom = typeof (IMapFrom<>); // Type 符合 IHaveCustomMapping 和 IMapFrom 的 predicate方法 // 這個predicate 的條件和下面個別mapping的第一個條件是一致的。 Func<Type, bool > predicate = (t => typeOfIHaveCustomMapping.IsAssignableFrom(t) // 找到符合IHaveCustomMapping || t.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeOfIMapFrom).Any()); // 找到符合IMapFrom<> var types = AssemblyTypes.GetAssemblyFromDirectory(assembly => assembly.GetExportedTypes().Where(predicate).Any()) // 選擇要讀進來的Assembly - 只有符合IHaveCustomMapping 和 IMapFrom才讀 // 把讀進來的Assembly取出裡面符合兩個interface的Type .SelectMany(x => x.GetExportedTypes() .Where(predicate)).ToList(); LoadStandardMappings(types); LoadCustomMappings(types); } } |
在來針對兩個不同的interface呼叫不同的mapping邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /// <summary> /// 註冊如果使用是自定義邏輯的Mapping /// </summary> /// <param name="types">可能符合的Type</param> private static void LoadCustomMappings(IEnumerable<Type> types) { var maps = (from t in types from i in t.GetInterfaces() where typeof (IHaveCustomMapping).IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface select (IHaveCustomMapping)Activator.CreateInstance(t)).ToArray(); foreach (var map in maps) { map.CreateMappings(AutoMapper.Mapper.Configuration); } } /// <summary> /// Loads the standard mappings. /// </summary> /// <param name="types">The types.</param> private static void LoadStandardMappings(IEnumerable<Type> types) { var maps = (from t in types from i in t.GetInterfaces() where i.IsGenericType && i.GetGenericTypeDefinition() == typeof (IMapFrom<>) && !t.IsAbstract && !t.IsInterface select new { Source = i.GetGenericArguments()[0], Destination = t }).ToArray(); foreach (var map in maps) { AutoMapper.Mapper.CreateMap(map.Source, map.Destination); } } |
Task Module
在上面的部分,如果注意看的話,AutoMapperConfig : IRunAtStartup
。而IRunAtStartup
其實屬於我們框架的Task系統。以IRunAtStartUp
來說,表示實作這個interface的Class將會在系統啟動的時候執行。
因此我們先設定這個Task的Autofac Module:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /// <summary> /// Autofac用來註冊Task相關的服務 /// </summary> public class TaskModule : Autofac.Module { /// <summary> /// Override to add registrations to the container. /// </summary> /// <param name="builder">The builder through which components can be /// registered.</param> /// <remarks> /// Note that the ContainerBuilder parameter is unique to this module. /// </remarks> protected override void Load(Autofac.ContainerBuilder builder) { var assemblies = Assembly.GetExecutingAssembly(); builder.RegisterAssemblyTypes(assemblies).As<IRunAtStartup>(); } } |
然後在Global.asax的地方註冊這個Module:
1 2 3 4 5 | // global.asax Application_Start ... Builder.RegisterModule<TaskModule>(); .. |
最後,因為這個IRunAtStartup
屬於系統啟動的時候執行,因此在同樣global.asax
裡面的Application_Start
,我們就會:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // global.asax Application_Start ... var container = builder.Build(); DependencyResolver.SetResolver( new AutofacDependencyResolver(container)); // 執行IRunAtStartUp的實作物件 using (var scope = container.BeginLifetimeScope()) { var runAtStartUpTasks = scope.Resolve<IEnumerable<IRunAtStartup>>(); foreach (var item in runAtStartUpTasks) { item.Execute(); } } |
結語
在這一篇我們把AutoMapper的對應設定邏輯利用2種interface把它抽到了和ViewModel一起定義。這樣的好處是我們只要看到ViewModel,就會知道他和那些Entity有對應關係。
希望透過這一篇,讓在使用AutoMapper的時候能夠更簡單,並且更容易使用。
在下一篇,我們來看如何透過Unit of Work和Repository Pattern把DB的溝通抽出來。