Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

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

2014-09-30 Tuesday

在上一篇我們介紹了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的溝通抽出來。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
2014-09-30 Tuesday
comments powered by Disqus