[.NET]Entity與DataTable互轉
前言
在系統介接的時候,總是會有新舊系統或是資料轉換上的需求,例如舊的模組只吃DataTable,或是需要將DataTable轉成Entity的集合。
這邊只是簡單寫幾個function來作轉換的動作,順便帶到一些reflection的小應用。若是需要用到手工設計ORM模組,請參考小朱大的系列文:[Data Access] ORM 原理 系列文。
前提
這邊針對的是簡單的Entity,也就是Entity的屬性都是.NET原生的型別,而非自訂的Entity型別。支援的型別如下:
範例
1. 透過Entity的Type,取得相對應的DataTable,將Property轉成對應的DataColumn
/// <summary>
/// Gets the entity to data table schema.
/// </summary>
/// <param name="entityType">Type of the entity.</param>
/// <returns>對應Entity屬性型別的DataTable</returns>
public static DataTable GetEntityToDataTableSchema(Type entityType)
{
var result = new DataTable();
var properties = entityType.GetProperties();
foreach (var property in properties)
{
var columnType = property.PropertyType;
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
//兩種寫法都可以,透過Nullable.GetUnderlyingType()比較簡潔。
//columnType = property.PropertyType.GetGenericArguments()[0];
columnType = Nullable.GetUnderlyingType(property.PropertyType);
}
var column = new DataColumn(property.Name, columnType);
result.Columns.Add(column);
}
return result;
}
說明:
當Entity的property type為Nullable<T>時,需要取得T的型別,並將DataColumn的DataType設定為T,因為DataColumn無法支援Nullable的型別。
- 透過IsGenericType與判斷GenericType的定義是否為Nullable<>的型別,來判斷這個property的型別是否為Nullable<T>。
- 透過Nullable.GetUnderlyingType()來取得Nullable<T>裡面的T。
測試:
用來測試的Person類別
public class Person
{
public string Name { get; set; }
public int? Age { get; set; }
public DateTime? Birthday { get; set; }
}
GetEntityToDataTableSchema的單元測試程式:
/// <summary>
///GetEntityToDataTableSchema 的測試
///</summary>
[TestMethod()]
public void GetEntityToDataTableSchemaTest()
{
//arrange
Type entityType = typeof(Person);
DataTable expected = new DataTable();
expected.Columns.Add(new DataColumn("Name", typeof(string)));
expected.Columns.Add(new DataColumn("Age", typeof(int)));
expected.Columns.Add(new DataColumn("Birthday", typeof(DateTime)));
//act
DataTable actual;
actual = Converter.GetEntityToDataTableSchema(entityType);
//assert
Assert.AreEqual(expected.Columns.Count, actual.Columns.Count);
Assert.AreEqual(expected.Columns[0].DataType, actual.Columns[0].DataType);
Assert.AreEqual(expected.Columns[1].DataType, actual.Columns[1].DataType);
Assert.AreEqual(expected.Columns[2].DataType, actual.Columns[2].DataType);
}
2.將Entity集合直接轉成對應的DataTable,並將值填入DataTable。(這邊會用到第一個方法)
/// <summary>
/// 將entities直接轉成DataTable
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
/// <param name="entities">entity集合</param>
/// <returns>將Entity的值轉為DataTable</returns>
public static DataTable EntityToDataTable<T>(IEnumerable<T> entities)
{
var result = GetEntityToDataTableSchema(typeof(T));
if (entities.Count() == 0)
{
return result;
}
var properties = typeof(T).GetProperties();
foreach (var entity in entities)
{
var dr = result.NewRow();
foreach (var property in properties)
{
dr[property.Name] = property.GetValue(entity, null).NullToDbNull();
}
result.Rows.Add(dr);
}
return result;
}
說明:
當Entity集合沒有資料的時候,僅回傳對應Entity的DataTable。透過property.GetValue來取得該Entity上對應的property值,填進datarow[ColumnName],這邊的ColumnName與PropertyName相同。
EntityToDataTable<T>的單元測試程式:
/// <summary>
///EntityToDataTable 的測試
///</summary>
public void EntityToDataTableTest_Person集合轉DataTable<T>()
{
//arrange
IEnumerable<Person> entities = new List<Person>
{
new Person{ Name="91", Age=31, Birthday=new DateTime(2011,11,11)},
new Person{ Name="Ruffy"},
new Person{ Name="Bill", Age=40},
new Person{ Name="林志玲", Birthday=new DateTime(1977,11,29)}
};
DataTable expected = new DataTable();
expected.Columns.Add(new DataColumn("Name", typeof(string)));
expected.Columns.Add(new DataColumn("Age", typeof(int)));
expected.Columns.Add(new DataColumn("Birthday", typeof(DateTime)));
expected.Rows.Add("91", 31, new DateTime(2011, 11, 11));
expected.Rows.Add("Ruffy");
expected.Rows.Add("Bill", 40);
expected.Rows.Add("林志玲", null, new DateTime(1977, 11, 29));
//act
DataTable actual;
actual = Converter.EntityToDataTable<Person>(entities);
//assert
Assert.AreEqual(expected.Rows.Count, actual.Rows.Count);
for (int i = 0; i < expected.Rows.Count; i++)
{
Assert.AreEqual(expected.Rows[i]["Name"], actual.Rows[i]["Name"]);
Assert.AreEqual(expected.Rows[i]["Age"], actual.Rows[i]["Age"]);
Assert.AreEqual(expected.Rows[i]["Birthday"], actual.Rows[i]["Birthday"]);
}
}
[TestMethod()]
public void EntityToDataTableTest_Person集合轉DataTable()
{
EntityToDataTableTest_Person集合轉DataTable<GenericParameterHelper>();
}
3.將DataTable內的資料,轉成對應的Entity集合
/// <summary>
/// 將DataTable轉換成對應的Entity集合
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dt"></param>
/// <returns></returns>
public static IEnumerable<T> DataTableToEntities<T>(DataTable dt)
{
foreach (DataRow row in dt.Rows)
{
T result = Activator.CreateInstance<T>();
foreach (DataColumn column in dt.Columns)
{
typeof(T).GetProperty(column.ColumnName).SetValue(result, row[column.ColumnName].DbNullToNull(), null);
}
yield return result;
}
}
說明:
使用Activator.CreateInstance<T>來初始化一個泛型T的instance。用ColumnName透過GetProperty()來取得對應的Property,透過SetValue來將DataRow的值assign給該Property。最後透過yield return來回傳IEnumerable<T>。
DataTableToEntities<T>的單元測試程式:
/// <summary>
///DataTableToEntities 的測試
///</summary>
public void DataTableToEntitiesTestHelper<T>()
{
//arrange
DataTable dt = new DataTable();
dt.Columns.Add(new DataColumn("Name", typeof(string)));
dt.Columns.Add(new DataColumn("Age", typeof(int)));
dt.Columns.Add(new DataColumn("Birthday", typeof(DateTime)));
dt.Rows.Add("91", 31, new DateTime(2011, 11, 11));
dt.Rows.Add("Ruffy");
dt.Rows.Add("Bill", 40);
dt.Rows.Add("林志玲", null, new DateTime(1977, 11, 29));
IList<Person> expected = new List<Person>
{
new Person{ Name="91", Age=31, Birthday=new DateTime(2011,11,11)},
new Person{ Name="Ruffy"},
new Person{ Name="Bill", Age=40},
new Person{ Name="林志玲", Birthday=new DateTime(1977,11,29)}
};
//act
IEnumerable<Person> actual;
actual = Converter.DataTableToEntities<Person>(dt);
var result = actual.ToList<Person>();
//assert
Assert.AreEqual(expected.Count, result.Count);
for (int i = 0; i < expected.Count; i++)
{
Assert.AreEqual(expected[i].Age, result[i].Age);
Assert.AreEqual(expected[i].Birthday, result[i].Birthday);
Assert.AreEqual(expected[i].Name, result[i].Name);
}
}
[TestMethod()]
public void DataTableToEntitiesTest()
{
DataTableToEntitiesTestHelper<GenericParameterHelper>();
}
補充說明
-
集合使用IEnumerable<T>的好處:
- 需要轉成任何集合型別時,IEumerable<T>已經有很好的Extension Method可以使用,例如Dictionary<T1, T2>, List<T>, Array<T>, LookUp<T1, T2>等等…而非IEnumerable<T>的集合,例如DataTable, 某些DataCollection (例如ListItemCollection, NameValueCollection),可以透過特殊方法來轉成IEnumerable<T>,例如AsEnumerable()或.Cast<T>來轉換。這樣可以讓使用端來決定,要將回傳的IEumerable<T>轉成什麼樣的資料結構,才是適合他的應用場景。提前轉換只會造成無謂的效能耗費。
- 由於許多資料結構有實作IEumerable<T>的介面,所以根據多型的原則,在使用場景需要傳入IEnumerable<T>的參數時,他可以用各式各樣的資料結構,只要有實作IEnumerable<T>即可,例如IList<T>, List<T>, Dictionary<T>等等…讓使用場景無須為了參數型別,而進行無謂的轉換,浪費效能。
- 用到的DbNull與Null互轉的Extension Method:
/// <summary>
/// 若值為DBNull.Value, 則轉為Null
/// </summary>
/// <param name="original"></param>
/// <returns></returns>
public static object DbNullToNull(this object original)
{
return original == DBNull.Value ? null : original;
}
/// <summary>
/// 若值為null, 則轉成DBNull.Value
/// </summary>
/// <param name="original"></param>
/// <returns></returns>
public static object NullToDbNull(this object original)
{
return original ?? DBNull.Value;
}
單元測試程式:
/// <summary>
///DbNullToNull 的測試
///</summary>
[TestMethod()]
public void DbNullToNullTest_針對DBNull()
{
object original = DBNull.Value;
object expected = null;
object actual;
actual = Converter.DbNullToNull(original);
Assert.AreEqual(expected, actual);
}
/// <summary>
///DbNullToNull 的測試
///</summary>
[TestMethod()]
public void DbNullToNullTest_針對有值()
{
int original = 91;
int expected = 91;
object actual;
actual = Converter.DbNullToNull(original);
Assert.AreEqual(expected, actual);
}
/// <summary>
///NullToDbNull 的測試
///</summary>
[TestMethod()]
public void NullToDbNullTest_針對Null()
{
object original = null;
object expected = DBNull.Value;
object actual;
actual = Converter.NullToDbNull(original);
Assert.AreEqual(expected, actual);
}
/// <summary>
///NullToDbNull 的測試
///</summary>
[TestMethod()]
public void NullToDbNullTest_針對有值()
{
int original = 91;
object expected = 91;
object actual;
actual = Converter.NullToDbNull(original);
Assert.AreEqual(expected, actual);
}
結論
這篇文章其實就是Reflection針對Property的使用,以及IEnumerable<T>的好處簡介。
附上單元測試的目的,在於希望讀者,可以透過單元測試的程式碼,來瞭解function的意義為何。以及培養一個好習慣,至少要有對應的單元測試程式,來證明程式在某個應用場景底下,是可以正常運作的。未來當出現非預期結果的問題時,則『將新的問題當作測試案例,建立單元測試,修復程式,測試通過』=修復完成。
Source Code : Utility.zip
堪誤(2012/12/2):DataTableToEntities<T>不需要取得properties。
blog 與課程更新內容,請前往新站位置:http://tdd.best/