[.NET]Entity與DataTable互轉

  • 24252
  • 0
  • 2011-12-02

[.NET]Entity與DataTable互轉

前言
在系統介接的時候,總是會有新舊系統或是資料轉換上的需求,例如舊的模組只吃DataTable,或是需要將DataTable轉成Entity的集合。

這邊只是簡單寫幾個function來作轉換的動作,順便帶到一些reflection的小應用。若是需要用到手工設計ORM模組,請參考小朱大的系列文:
[Data Access] ORM 原理 系列文

前提
這邊針對的是簡單的Entity,也就是Entity的屬性都是.NET原生的型別,而非自訂的Entity型別。支援的型別如下:

image

範例
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>();
        }


補充說明

  1. 集合使用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>等等…讓使用場景無須為了參數型別,而進行無謂的轉換,浪費效能。
  2. 用到的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);
        }

測試結果
image


參考

  1. HOW TO:識別可為 Null 的型別 (C# 程式設計手冊)
  2. Nullable.GetUnderlyingType 方法
  3. PropertyInfo 類別


結論
這篇文章其實就是Reflection針對Property的使用,以及IEnumerable<T>的好處簡介。

附上單元測試的目的,在於希望讀者,可以透過單元測試的程式碼,來瞭解function的意義為何。以及培養一個好習慣,至少要有對應的單元測試程式,來證明程式在某個應用場景底下,是可以正常運作的。未來當出現非預期結果的問題時,則『將新的問題當作測試案例,建立單元測試,修復程式,測試通過』=修復完成。

Source Code : Utility.zip


堪誤(2012/12/2):DataTableToEntities<T>不需要取得properties。


blog 與課程更新內容,請前往新站位置:http://tdd.best/