在以前的項目開發之中,經常會遇到這樣一個問題:比如在外面項目的架構設計之中,我們采用MVC和EntityFramework來構建一個Web應用程序。比如我們采用常用的多層架構,例如有Presentation層、BusinessLogic層、DataAccess層等,各層之間是相對獨立並且職責分明的。比如我們在Presentation層中會定義ViewModel,在DataAccess層中的DbContext部分又會由EntityFramework來自動生成StorageModel,或者叫做DataModel。然后我們從DataAccess層從數據庫抓取到數據之后需要將這些數據傳遞給viewModel,並最終呈現給前段用戶,當然兩種Model之間定義的字段(屬性)可能會有所區別,這個我們將會在稍后討論。
我們先來看看如何解決這一類問題。首先最朴素笨拙的辦法就是,逐個屬性的為對象賦值,例如這樣:
var viewModels = new List<EmployeeViewModel>(); List<EmployeeStorageModel> storageModels = new List<EmployeeStorageModel>(); if (storageModels.Count > 0) { EmployeeViewModel viewModel = null; foreach (var storageModel in storageModels) { viewModel.Number = storageModel.Name; viewModel.Name = storageModel.Name; viewModel.HireDate = storageModel.HireDate; viewModel.Job = storageModel.Job; viewModel.Department = storageModel.Department; viewModel.Salary = storageModel.Salary; //.... } }
如果對象的屬性比較多,那么我們就不得不寫一長串的賦值語句,這顯示不是一個聰明的辦法。說了這么多目的是為了拋出一個問題。那么解決方案呢?對於這個問題有許多的解決方案,比如使用AutoMapper等,之前我記得我們一個同事寫了一個擴展方法是使用JSON的序列化和反序列化來實現對象數據成員之間的映射,不過我今天要拿出來說的是使用映射來解決這個問題。
我想要實現的功能如下,或者說我要解決的問題吧:
1. 實現兩個不同對象數據成員的映射,數據成員包括屬性和公共可寫字段;
2. 將具有相同名稱和數據類型的數據成員映射到另外另外一個對象;
在這里我先給出核心代碼,后面將會對業務邏輯和一些注意問題進行詳細的說明:
第一、定義一個Attribute,這個特性主要用來標示類的數據成員的別名,后面我將介紹為什么要怎么做
//數據成員的別名 [AttributeUsage(AttributeTargets.Property| AttributeTargets.Field, AllowMultiple=false, Inherited=true)] public class DataMemberAliasAttribute : System.Attribute { private readonly string _alias; public DataMemberAliasAttribute(string alias) { _alias = alias; } public string Alias { get { return _alias; } } }
第二、定義一個類,這個類包含一個公共的靜態方法Mapping,這個方法是一個擴展方法,后面將會詳細介紹為什么要這么定義
private static T Mapping<T>(this object source) where T : class { Type t = typeof(T); if (source == null) { return default(T); } T target = (T)t.Assembly.CreateInstance(t.FullName); #region Mapping Properties PropertyInfo[] targetProps = t.GetProperties(); if (targetProps != null && targetProps.Length > 0) { string targetPropName; //目標屬性名稱 PropertyInfo sourceProp; object sourcePropValue; foreach (PropertyInfo targetProp in targetProps) { //優先使用數據成員的別名,如果沒有別名則使用屬性名 object[] targetPropAliasAttrs = targetProp.GetCustomAttributes(typeof(DataMemberAliasAttribute), true); if (targetPropAliasAttrs != null && targetPropAliasAttrs.Length > 0) targetPropName = ((DataMemberAliasAttribute)targetPropAliasAttrs[0]).Alias; else targetPropName = targetProp.Name; //檢索源屬性 sourceProp = source.GetType().GetProperty(targetPropName); if (sourceProp != null && sourceProp.CanRead && targetProp.CanRead) { sourcePropValue = sourceProp.GetValue(source, null); //屬性類型一致時,直接填充屬性值 if (targetProp.PropertyType == sourceProp.PropertyType) targetProp.SetValue(target, sourcePropValue, null); } } } #endregion #region Mapping Fields FieldInfo[] targetFields = t.GetFields(); if (targetFields!=null&&targetFields.Length>0) { string targetFieldName; FieldInfo sourceField; foreach (FieldInfo targetField in targetFields) { if (!targetField.IsInitOnly && !targetField.IsLiteral) {//字段可以被賦值 object[] targetFieldAttrs = targetField.GetCustomAttributes(typeof(DataMemberAliasAttribute), true); if (targetFieldAttrs != null && targetFieldAttrs.Length > 0) targetFieldName = ((DataMemberAliasAttribute)targetFieldAttrs[0]).Alias; else targetFieldName = targetField.Name; sourceField = source.GetType().GetField(targetFieldName); if (sourceField!=null) { //數據類型相同時映射值 if (targetField.FieldType == sourceField.FieldType) targetField.SetValue(target, sourceField.GetValue(source)); } } } } #endregion return target; } public static TOut Mapping<TOut,TIn>(this TIn source) where TIn :class where TOut :class { return source.Mapping<TOut>(); } }
第三、然后我就可以使用下面這種語法來進行兩個對象之間數據成員的映射:
EmployeeViewModel viewmodel = storageModel.Mapping<EmployeeViewModel, EmployeeStorageModel>();
好的,代碼已經全部貼出來了。那么我先來介紹一下第一個問題:為什么要定義DataMemberAliasAttribute這個特性類。人們在做一件事情的時候通常都是為了解決某一類問題的,同樣,之所以這么做,是為了適應兩個對象(類)包含的數據成員的名字可能不相同。就拿上面的例子來說,在上面的EmployeeStorageModel中可能或包含一個Property,叫做hire_date,表示員工的雇用日期,但是在EmployeeViewModel類中可能會有一個叫做HireDate的屬性與之對應。如果我們僅僅使用屬性本身的名字來進行映射的話這是不夠的,而且這種情況經常會發生,因為我們的StorageModel通常可能是有EntityFramework自動生成的,他么的名稱通常與數據庫表中的字段的名稱相同,而這完全取決於數據庫表設計人員的設計習慣,但是ViewModel很有可能是某一個.NET程序員設計的,他很有可能會按照微軟建議的屬性命名方法來進行屬性的命名,例如每個單詞的首字母都為大寫。所以我在這里定義了這個特性類,用來表示某個數據成員的“別名”,使用方法如下:
public class EmployeeViewModel { public const string phoneNumber = ""; public readonly string emailAddress; [DataMemberAlias("id")] public int Id { get; set; } [DataMemberAlias("employee_number")] public string EmployeeNumber { get; set; } [DataMemberAlias("employee_name")] public string EmployeeName { get; set; } [DataMemberAlias("hire_date")] public DateTime HireDate { get; set; } [DataMemberAlias("salary")] public double Salary { get; set; } [DataMemberAlias("job")] public string Job { get; set; } [DataMemberAlias("department")] public Department Department { get; set; } public int Status { get; set; } }
這樣就可以解決上面提出的問題。注意如果在對象類中為數據成員設置了別名,那么在進行映射時,會優先匹配別名;如果沒有為數據成員按設置別名,那么就會匹配數據成員本身的名稱。同時需要注意以下細節:
1、對象B的屬性需要可寫,對象A的屬性需要可讀,否則將會忽略此數據成員;
2、對象B的字段需要可以被賦值,即不能帶readonly或者const修飾符,因為這樣的話,無法為字段進行賦值,只能忽略它們;
3、只映射公共的屬性和字段(通常屬性都為public,而帶有public修飾符的字段也具有屬性的一些特征)
4、對象A和對象B相同數據成員的數據類型必須相同;
5、如果對象A和對象B中的數據成員的數據類型相同且都為引用類型,那么傳遞是是引用,在使用中需要注意這一點,請見Department屬性的示例代碼
[DataMemberAlias("department")] public Department Department { get; set; }
public class Department { public int DepartmentCode { get; set; } public string DepartmentName { get; set; } public string DepartmentLeader { get; set; } }
現在我們來看一下Mapping這個擴展方法,首先這個方法是一個番型方法,它包含兩個類型參數TIn和TOut,TOut為方法返回值的數據類型,及對象B的數據類型,TIn為參數的數據類型,即為對象A的數據類型。同時限定這兩個類型參數在實例化的時候必須指定為類類型,這一點是出於對應用場景的設計。因為這是一個擴展方法,所以可以使用下面的語法來進行方法的調用:
EmployeeViewModel viewmodel = storageModel.Mapping<EmployeeViewModel, EmployeeStorageModel>();
本來想講一下Mapping方法的業務邏輯的,但是因為時間的關系,在這里就不再細說了,代碼里面都有注釋。大家如果有興趣可以加我微信:Happy_Chopper
------------------------------------------------------------------------------------------------------------------------------------End