在上一篇我們介紹了AutoMapper的設定和用法,使用起來肯定比自己手動做左邊倒到右邊還要簡單。
不過AutoMapper也不是沒有它自己的問題,最麻煩的地方在於設定Entity和Class之間的對應。這一篇要探討的就是,如何透過框架來減少這方面的設定。
我們先來思考一下我們會如何達到簡化設定對應邏輯,然後在開始開發。
首先,其實AutoMapper本身有所謂的Profile,可以透過Profile來設定Entity和ViewModel之間的對應。不過我個人比較傾向於Entity和ViewModel的對應邏輯是能夠簡單看到並且是在一起,換句話說,如果能夠在ViewModel定義好和Entity的對應關係不是很好,因為只要一找到ViewModel,馬上就知道它和Entity的關係。
有了這個概念,我們就可以來看一下我們如何透過Interface來達到這個效果。
我們要提供兩種定義的方式:
因此看起來會是:
interface的Class diagram
然後實際的C#程式碼是:
/// <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);
}
我們先看一下上一篇我們IndexViewModel本來的用法:
Mapper.CreateMap<Post, IndexViewModel>();
var projectIQueryable = (db.Post.Project().To<IndexViewModel>()).ToList();
如果改用成我們的interface,會變成:
// ViewModel加上interface
public class IndexViewModel : IMapFrom<Post>
....
// 在實際呼叫的時候,會和之前一樣,只是不需要呼叫CreatMap
var projectIQueryable = (db.Post.Project().To<IndexViewModel>()).ToList();
這個是假設有特殊的對應邏輯才在呼叫,使用上會是:
public class IndexViewModel : IHaveCustomMapping
{
// properties
public void CreateMappings(IConfiguration configuration)
{
configuration.CreateMap<Post, IndexViewModel>();
}
}
可以看到,AutoMapper的IConfiguration會被傳進來,這時候就可以手動設定對應邏輯。
到這邊為止,我們interface的定義和使用就完成了,不過接下來我們還需要讓這兩個interface實際有作用,要不然是沒有效果。
當我們用了interface把這些ViewModel的對應都定義好了之後,我們希望在系統啟動了之後,讀出所有設定過這兩種interface的ViewModel,並且作出對應的AutoMapper設定。
我們首先寫好使用這兩個interface的邏輯:
顯示取得所有實作這兩個interface的type:
/// <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邏輯:
/// <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);
}
}
在上面的部分,如果注意看的話,AutoMapperConfig : IRunAtStartup。而IRunAtStartup其實屬於我們框架的Task系統。以IRunAtStartUp 來說,表示實作這個interface的Class將會在系統啟動的時候執行。
因此我們先設定這個Task的Autofac Module:
/// <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:
// global.asax Application_Start
...
Builder.RegisterModule<TaskModule>();
..
最後,因為這個IRunAtStartup屬於系統啟動的時候執行,因此在同樣global.asax裡面的Application_Start,我們就會:
// 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();
}
}
這樣我們有設定的那兩種interface Mapping的AutoMapper定義就會有效果了。
在這一篇我們把AutoMapper的對應設定邏輯利用2種interface把它抽到了和ViewModel一起定義。這樣的好處是我們只要看到ViewModel,就會知道他和那些Entity有對應關係。
希望透過這一篇,讓在使用AutoMapper的時候能夠更簡單,並且更容易使用。
在下一篇,我們來看如何透過Unit of Work和Repository Pattern把DB的溝通抽出來。