2014年9月30日 星期二

[iThome 第七屆鐵人賽 08] 框架簡化建立AutoMapper對應的設定

在上一篇我們介紹了AutoMapper的設定和用法,使用起來肯定比自己手動做左邊倒到右邊還要簡單。

不過AutoMapper也不是沒有它自己的問題,最麻煩的地方在於設定Entity和Class之間的對應。這一篇要探討的就是,如何透過框架來減少這方面的設定。

框架思路

我們先來思考一下我們會如何達到簡化設定對應邏輯,然後在開始開發。

首先,其實AutoMapper本身有所謂的Profile,可以透過Profile來設定Entity和ViewModel之間的對應。不過我個人比較傾向於Entity和ViewModel的對應邏輯是能夠簡單看到並且是在一起,換句話說,如果能夠在ViewModel定義好和Entity的對應關係不是很好,因為只要一找到ViewModel,馬上就知道它和Entity的關係。

有了這個概念,我們就可以來看一下我們如何透過Interface來達到這個效果。

Interface的定義

我們要提供兩種定義的方式:

  1. IMapFrom<T> - T表示這個ViewModel對應的Entity
  2. IHaveCustomMapping - 表示這個ViewModel要自己對應Entity和設定自己的邏輯

因此看起來會是:

image
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);
}

使用兩個interface的差異


使用IMapFrom<T>


我們先看一下上一篇我們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();

使用IHaveCustomMapping


這個是假設有特殊的對應邏輯才在呼叫,使用上會是:

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:

/// <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);
}
}


Task Module


在上面的部分,如果注意看的話,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的溝通抽出來。


1 則留言 :

  1. 您好,這邊文章似乎是基於AutoMapper 4.0的寫法,
    但AutoMapper 5.0的語法已經改變,不知是否有推薦的改寫方式?

    我自己只想到每個ViewModel自行實作ToModel()這種基本作法而已

    回覆刪除