IEnumerable<T>からDataTableに変換する

LinqはサイコーだしLinqToObjectはサイコー。なのに使用を強いられるレガシーなライブラリの受け入れがDataTableやDataSetでその部分だけクソレガシーなsqlを書かなきゃいけないのが大変にストレス。

そんな場合に使えるIEnumerable<T>→DataTable変換

## コード
```c#:ExtensionForDataTable.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace qyen.tools {
    public static class ExtensionForDataTable {
        private const string FieldConnectorChar = "_";
        /// <summary>
        /// Convert IEnumerable<T> To System.Data.DataTable
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="datas"></param>
        /// <param name="expandPropertyOptions"></param>
        /// <returns></returns>
        public static System.Data.DataTable ToDataTable<T>(this IEnumerable<T> datas, ExpandPropertyOptions expandPropertyOptions = null) {
            return datas.ToDataTable(expandPropertyOptions != null, expandPropertyOptions);
        }
        /// <summary>
        /// Convert IEnumerable<T> To System.Data.DataTable
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="datas"></param>
        /// <param name="ExpandNonPrimitiveClass"></param>
        /// <param name="expandPropertyOptions"></param>
        /// <returns></returns>
        public static System.Data.DataTable ToDataTable<T>(this IEnumerable<T> datas, bool ExpandNonPrimitiveClass = false, ExpandPropertyOptions expandPropertyOptions = null) {
                var baseType = typeof(T);
            var hlist = baseType.BuildPropertyList(ExpandNonPrimitiveClass, expandPropertyOptions);
            var res = new System.Data.DataTable();
            //build DataTable Schema
            foreach (var prop in hlist.Where(x => !hlist.Any(y => y.parent == x))) {
                res.Columns.Add(new System.Data.DataColumn() {
                    ColumnName = prop.PropertyName,
                    DataType = NonNullable(prop.Property.PropertyType),
                });
            }
            //Add Contents
            foreach (var data in datas) {
                var row = res.NewRow();
                foreach (var prop in hlist.Where(x => !hlist.Any(y => y.parent == x))) {
                    var d = prop.GetHierarchicalValue(data);
                    if (d != null)
                        row[prop.PropertyName] = d;
                }
                res.Rows.Add(row);
            }
            return res;
        }
        /// <summary>
        /// Build hierarchical Property Info
        /// </summary>
        /// <param name="baseType"></param>
        /// <param name="ExpandNonPrimitiveClass"></param>
        /// <param name="expandPropertyOptions"></param>
        /// <param name="parent"></param>
        /// <returns></returns>
        internal static IEnumerable<HierarchyPropertyNode> BuildPropertyList(this Type baseType, bool ExpandNonPrimitiveClass = false, ExpandPropertyOptions expandPropertyOptions = null, HierarchyPropertyNode parent = null) {
            if (expandPropertyOptions == null) {
                expandPropertyOptions = new ExpandPropertyOptions();
            }
            var props = baseType.GetProperties();
            var res = new List<HierarchyPropertyNode>();
            foreach (var prop in props) {
                var hpn = new HierarchyPropertyNode() {
                    parent = parent,
                    Property = prop,
                };
                res.Add(hpn);
                if (ExpandNonPrimitiveClass
                    && prop.PropertyType.CanExpand()
                    && expandPropertyOptions.CanExpand(hpn)) {
                    res.AddRange(prop.PropertyType.BuildPropertyList(ExpandNonPrimitiveClass, expandPropertyOptions, hpn));
                }
            }
            return res;
        }
        /// <summary>
        /// Option settings
        /// </summary>
        public class ExpandPropertyOptions {
            public bool ExpandCircularReference { get; set; } = false;
            public int MaxReferenceLevel { get; set; } = 3;
            public IList<string> IncludeProperties { get; private set; } = new List<string>();
            public ExpandPropertyOptions Include(string Property) {
                if (!string.IsNullOrEmpty(Property))
                    IncludeProperties.Add(Property.Replace(".", "_"));
                return this;
            }
            internal bool CanExpand(HierarchyPropertyNode hpn) {
                bool result = true;
                if (hpn.Level >= MaxReferenceLevel)
                    result = false;
                if (!ExpandCircularReference && hpn.IsCircularReference)
                    result = false;
                if (IncludeProperties.Any() && !IncludeProperties.Contains(hpn.PropertyName))
                    result = false;
                return result;
            }
        }

        /// <summary>
        /// Hierarchical Property Info
        /// </summary>
        /// <remarks>
        /// parent : key
        /// </remarks>
        internal class HierarchyPropertyNode {
            public HierarchyPropertyNode parent { get; set; }
            public PropertyInfo Property { get; set; }
            public string PropertyName {
                get {
                    return string.Join(FieldConnectorChar, this.Hierarchy.Select(x => x.Property.Name));
                }
            }
            private IEnumerable<HierarchyPropertyNode> Parents {
                get {
                    var current = this.parent;
                    var res = new List<HierarchyPropertyNode>();
                    while (current != null) {
                        res.Add(current);
                        current = current.parent;
                    }
                    res.Reverse();
                    return res;
                }
            }
            private IEnumerable<HierarchyPropertyNode> Hierarchy {
                get {
                    var list = Parents.ToList();
                    list.Add(this);
                    return list;
                }
            }
            internal int Level {
                get {
                    return Hierarchy.Count();
                }
            }
            internal bool IsCircularReference {
                get {
                    return ParentHasSameType(Property.PropertyType);
                }
            }
            private bool ParentHasSameType(Type type) {
                return Parents.Any(x => x.Property.PropertyType == type);
            }

            /// <summary>
            /// Get value from root object
            /// </summary>
            /// <param name="root"></param>
            /// <returns>if any parent gets null,return null </returns>
            internal object GetHierarchicalValue(object root) {
                object value = root;
                foreach (var h in Hierarchy) {
                    try {
                        value = h.Property.GetValue(value, null);
                    } catch {
                        return null;
                    }
                }
                return value;
            }
        }
        /// <summary>
        /// If Type is Nullable<T> return T,else return t
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private static Type NonNullable(Type t) {
            return Nullable.GetUnderlyingType(t) ?? t;
        }

        /// <summary>
        /// check t is Nullable
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private static bool IsNullable(Type t) {
            return Nullable.GetUnderlyingType(t) != null;
        }

        /// <summary>
        /// 
        /// </summary>
        private static Type[] ExcludeExpands = new Type[] { typeof(DateTime), typeof(string), typeof(decimal) };
        /// <summary>
        /// Check objClass can expand
        /// </summary>
        /// <param name="objClass"></param>
        /// <returns></returns>
        private static bool CanExpand(this Type objClass) {
            return !objClass.IsPrimitive
                && !ExcludeExpands.Contains(objClass)
                && !IsNullable(objClass)
                && !objClass.GetInterfaces().Any(t => t == typeof(IEnumerable<>));
        }
    }
}
```

## つかいかた

```c#

public class A{
	public string Name{get;set;}
	public decimal DecimalValue{get;set;}
	public B b{get;set;}
	public C c{get;set;}
}

public class B{
	public string Name{get;set;}
	public DateTime Date{get;set;}
	public C c{get;set;}
}

public class C{
	public string Name{get;set;}
	public A a{get;set;}
}

void Main(){
	var cList = Enumerable.Range(0,3).Select(x=>new C(){Name=$"C of{x}"}).ToList();
	var bList = Enumerable.Range(0,5).Select(x=>new B(){Name=$"B of {x}",Date=DateTime.Today.AddDays(x),c=cList[x%2]}).ToList();
	var aList = Enumerable.Range(0,10).Select(x=>new A(){
		Name=x.ToString(),
		DecimalValue =(decimal)(x*1.121),// ←てきとう
		b = bList[x % 5],
		c=cList[x % 3]
	});
}
```
みたいな場合。

### 特にオブジェクトを展開しないで単純にDataTableにしたい

```c#
  var dataTable = aList.ToDataTable();
```
![スクリーンショット (5).png](https://qiita-image-store.s3.amazonaws.com/0/199750/2f8dd099-a6ee-e986-3dbb-91b24a1009a4.png)
オブジェクトはそのままDataTableに格納される。

### オブジェクトを展開してDataTableに
```c#
  var dataTable = aList.ToDataTable(true);
```
![スクリーンショット (6).png](https://qiita-image-store.s3.amazonaws.com/0/199750/8761eb5c-946f-d7dc-7479-b7fbc368d150.png)

### 1階層だけ展開してDataTableに
```c#
  var option = new ExpandPropertyOptions(){MaxReferenceLevel=2}; //2階層目までOK
  var dataTable = aList.ToDataTable(true,option);
```
![スクリーンショット (7).png](https://qiita-image-store.s3.amazonaws.com/0/199750/bee79f6e-e996-8485-fde4-6552052b651a.png)

a.b a.cだけ展開されてそれ以降のa.b.c a.c.a は展開されていない 

### 特定のプロパティだけ展開してDataTableにしたい
```c#
  var option = new ExpandPropertyOptions().Include("b").Include("c").Include("c_a");
  var dataTable = aList.ToDataTable(true,option);
```
![スクリーンショット (8).png](https://qiita-image-store.s3.amazonaws.com/0/199750/27a9dd67-5263-d117-5c0c-a648d036f428.png)

## 参考

- [Converting an Object to dataTable in C\# \| The ASP\.NET Forums](https://forums.asp.net/t/904608.aspx?Converting+an+Object+to+dataTable+in+C+)
- [DataTableでstring以外の型を持つカラムにnullを許可したい](https://social.msdn.microsoft.com/Forums/vstudio/ja-JP/3048dc71-d2dd-43bf-b617-18fa8354ceba/datatablestringnull?forum=vsgeneralja)
- [c\# \- Correct way to check if a type is Nullable \- Stack Overflow](https://stackoverflow.com/questions/8939939/correct-way-to-check-if-a-type-is-nullable)