[C#.NET] Use Reflection / AutoMapper achieve Object Mapping
在三層式架構中,中間插了 ViewModel ,ViewModel 是由 UI 所呈現的資料設計出來,ViewModel 與 Model 會不斷的轉換,達到資料交換、呈現,這樣的架構令不少開發人員疑惑:為什麼 ViewModel 的欄位跟 Model 差不多,還要多一個 ViewModel,直接把 Model 放在 UI 不行嗎?
Entity Model 包含了所有資料庫的欄位,UI 可能跟本不需要 Model 所有的欄位,直接綁定到 UI 則是造成了網路資源的浪費;為了要讓 UI 的欄位也能在使用 Entity Model,資料庫又會把 UI 的欄位加進去 ,UI 又是最常修改的,全部攪成一團,真的是很痛苦
ViewModel 解決了 UI 與 Model 的直接依賴,讓架構更有彈性、提高開發效率,UI、資料庫和商業邏輯並行開發、提高可測試性、避免SQL欄位訊息洩露,所以通常不會在 UI 直接使用 Entity Model,當然這不是絕對的,還是要看使用情境
本篇情境:
解決手動處理 Model to ViewModel、ViewModel to Model
本篇章節:
ViewModel 很好,但手動處理對應關係,真的是件苦工,欄位越多,漏打的機會就越高,錯誤越高,開發人員越抗拒,明明就一樣,為什麼要寫兩次,如下程式碼:
{ var account = new Account(); account.UserId = accountViewModel.UserId; account.Password = accountViewModel.Password; //TODO:後面還有好多欄位 return account; }
上面的例子還好,只要懂的例用反射,上面的問題很容易解掉,如下程式碼
TSource 代表的是來源物件,迴圈是依照 TSource 的欄位數量去執行的
TTarget 代表目標物件
where TSource : class,new() { var sourceType = sourceInstance.GetType(); var targetType = typeof(TTarget); var sourceProperties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public); var targetProperties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); var targetInstance = Activator.CreateInstance<TTarget>(); foreach (var sourceProperty in sourceProperties) { var sourcePropertyName = sourceProperty.Name; var sourceValue = sourceProperty.GetValue(sourceInstance); foreach (var targetProperty in targetProperties) { var targetPropertyName = targetProperty.Name; if (sourcePropertyName == targetPropertyName) { if (sourceProperty.PropertyType == targetProperty.PropertyType) { targetProperty.SetValue(targetInstance, sourceValue); break; } } } } return targetInstance; }
ViewModel 跟 Entity Model 資料庫實體名稱相同, 就達不到資訊隱藏的效果,所以一般會建議將它們兩個的欄位名稱取不一樣,也就是上面的邏輯不能用了,要再改一下,我想要在 ViewModel 屬性上加一個 Map Attribute,用它來決定映射的欄位名稱
@ObjectMappingAttribute.cs
首先,先增加一個 ObjectMappingAttribute
public class ObjectMappingAttribute : Attribute { public string MappingName { get; set; } public ObjectMappingAttribute(string mappingName) { this.MappingName = mappingName; } }
@Account.cs
Account 是 Entity Model,它會對應到資料庫
{ public Guid Id { get; set; } public string UserId { get; set; } public string Password { get; set; } }
@AccountTwViewModel.cs
AccountTwViewModel 是提供給 UI 綁定的物件
TwViewModel { [ObjectMapping("UserId")] public string 帳號 { get; set; } [ObjectMapping("Password")] public string 密碼 { get; set; } }
@Utility.cs
Migration 方法用來處理資料對應,跟上面的 Migration 方法多了 Attribute 的處理
where TSource : class,new() { var sourceType = sourceInstance.GetType(); var targetType = typeof(TTarget); var sourceProperties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public); var targetProperties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public); var targetInstance = Activator.CreateInstance<TTarget>(); var mappingAttributeType = typeof(ObjectMappingAttribute); foreach (var sourceProperty in sourceProperties) { var sourcePropertyName = sourceProperty.Name; var sourceValue = sourceProperty.GetValue(sourceInstance); foreach (var targetProperty in targetProperties) { var targetPropertyName = targetProperty.Name; if (sourcePropertyName == targetPropertyName) { if (sourceProperty.PropertyType == targetProperty.PropertyType) { targetProperty.SetValue(targetInstance, sourceValue); break; } } var mappingAttributes = targetProperty.GetCustomAttributes(mappingAttributeType, false); if (mappingAttributes.Any()) { var mappingAttributePropertyName = ((ObjectMappingAttribute)mappingAttributes[0]).PropertyName; if (mappingAttributePropertyName == sourcePropertyName) { if (sourceProperty.PropertyType == targetProperty.PropertyType) { targetProperty.SetValue(targetInstance, sourceValue); break; } } } } } return targetInstance; }調用 Migration 方法
public void Migration_Account_AccountTwViewModel_Test() { Account source = new Account(); source.UserId = "yao123"; source.Password = "1234"; Utility utility = new Utility(); var target = utility.Migration<Account, AccountTwViewModel>(source); Assert.AreEqual(source.UserId, target.帳號); Assert.AreEqual(source.Password, target.密碼); }
完整測試程式碼:
AutoMapper 使用起來非常簡單,網路上有很多的資源,從 Nuget Server 上可看到,這已經被很多人下載使用了
若兩個物件欄位名稱相同,它會幫我們處理兩個物件的內容
- Mapper.CreateMap 方法,用來宣告要處理的兩個物件型別,它的方法簽章如下:
public static IMappingExpression<TSource, TDestination> CreateMap<TSource, TDestination>();
- Mapper.Map 方法,用來處理兩個物件的內容對應,它的方法簽章如下:
public static TDestination Map<TSource, TDestination>(TSource source);測試程式碼如下:
public void Account_AccountViewModel_Test() { Account account = new Account(); account.UserId = "yao123"; account.Password = "1234"; Mapper.CreateMap<Account, AccountViewModel>(); var actual = Mapper.Map<Account, AccountViewModel>(account); Assert.AreEqual(account.UserId, actual.UserId); Assert.AreEqual(account.Password, actual.Password); }
若兩個物件欄位名稱不相同,要跟 AutoMapper 講對應關係或是不想要對應,ForMember 方法就是在做這件事,它的方法簽章如下:
IMappingExpression<TSource, TDestination> ForMember(Expression<Func<TDestination, object>> destinationMember, Action<IMemberConfigurationExpression<TSource>> memberOptions);使用方式一樣跟上面一樣,只是多了 ForMember
public void Account_AccountTwViewModel_Test() { Account account = new Account(); account.UserId = "yao123"; account.Password = "1234"; Mapper.CreateMap<Account, AccountTwViewModel>() .ForMember(target => target.帳號, option => option.MapFrom(source => source.UserId)) .ForMember(target => target.密碼, option => option.MapFrom(source => source.Password)); var actual = Mapper.Map<Account, AccountTwViewModel>(account); Assert.AreEqual(account.UserId, actual.帳號); Assert.AreEqual(account.Password, actual.密碼); }
文章出自:https://www.dotblogs.com.tw/yc421206/archive/2014/12/12/147617.aspx
範例專案:https://dotblogsamples.codeplex.com/SourceControl/latest#Simple.AutoMapViewModel/
運行測試:Ctrl + R,Ctrl + A
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET