IEnumerable<T> を DataTable に変換する
このご時世に DataTable かよ、という感じもしますし、私自身実案件では Dapper をメインで使用しているわけですが、少しは使える場面もあるかもしれません。
例えばデバッガで使える DataSet ビジュアライザ。こういうやつですね。
これなら複数レコードのプロパティ/フィールドの値を一覧で見ることができます。単に CSV やタブ区切りテキストに吐き出すという手もありますが、デバッグ中にシームレスに見られるというのはやはり便利です。少々重いですが。
他はまぁ、WinForms や WebForms を使っている場合ですかね。特に前者の場合、DataGridView のソートやフィルタを使うには IBindingList や IBindingListView インターフェースが必要なわけですが、これらを実装しているのはベースクラスライブラリでは DataView だけという状況で、自前で実装するのも相当骨が折れるので。
というわけで、いわゆる業務ロジックや DB アクセスには POCO を使い、UI でグリッドを使う時だけ DataTable に変換するという使い方ができるかもしれません(?)
というわけで作ってみましょう。まずはプロパティやフィールドの情報を基に DataColumn を作成するメソッドを用意します。
private static DataColumn ToDataColumn(this PropertyInfo propInfo) { return ToDataColumnCore(propInfo.PropertyType, propInfo.Name); } private static DataColumn ToDataColumn(this FieldInfo fieldInfo) { return ToDataColumnCore(fieldInfo.FieldType, fieldInfo.Name); } private static DataColumn ToDataColumnCore(Type type, String name) { var isNullable = (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); var columnType = isNullable ? Nullable.GetUnderlyingType(type) : type; var column = new DataColumn(name, columnType); column.AllowDBNull = (isNullable || !type.IsValueType); return column; }
私はわりとカジュアルに拡張メソッド作っちゃう派です。どうせ private だし。
型が Nullable ないし参照型の場合は DBNull を許すようにしてます。
あとは IEnumerable
引数に BindingFlags を指定することで、プロパティかフィールドのどちらか片方だけ出力することもできるようなオーバーロードも付けてます。
public static DataTable ToDataTable<T>(this IEnumerable<T> entities) { return entities.ToDataTable(BindingFlags.GetField | BindingFlags.GetProperty); } public static DataTable ToDataTable<T>(this IEnumerable<T> entities, BindingFlags bindingAttr) { if ((bindingAttr & ~BindingFlags.GetField & ~BindingFlags.GetProperty) != BindingFlags.Default) { throw new ArgumentException(nameof(bindingAttr)); } var table = new DataTable(typeof(T).Name); var getters = new Dictionary<DataColumn, Func<T, object>>(); var hasItem = entities.Any(); Action<Func<DataColumn>, Func<ParameterExpression, MemberExpression>> buildConverter = (getColumn, getMember) => { var column = getColumn(); table.Columns.Add(column); if (hasItem) { var param = Expression.Parameter(typeof(T), "entity"); var member = Expression.Convert(getMember(param), typeof(object)); var lambda = Expression.Lambda<Func<T, object>>(member, param); getters.Add(column, lambda.Compile()); } }; if ((bindingAttr & BindingFlags.GetProperty) == BindingFlags.GetProperty) { foreach (var property in typeof(T).GetProperties()) { buildConverter(() => property.ToDataColumn(), (param) => Expression.Property(param, property)); } } if ((bindingAttr & BindingFlags.GetField) == BindingFlags.GetField) { foreach (var field in typeof(T).GetFields()) { buildConverter(() => field.ToDataColumn(), (param) => Expression.Field(param, field)); } } foreach (var entity in entities) { var row = table.NewRow(); row.BeginEdit(); foreach (var getter in getters) { row[getter.Key] = getter.Value(entity) ?? DBNull.Value; } row.EndEdit(); table.Rows.Add(row); } table.AcceptChanges(); return table; }
本当はもう少しオーバーロード追加したり、テストメソッドも用意した上で GitHub に上げたかったんですが、ただの Git すらよくわかってないマンなので、今日のところはこれまで。