TTYF ~earlgrey の雑記~

主に自分用メモとか

IEnumerable<T> を DataTable に変換する

このご時世に DataTable かよ、という感じもしますし、私自身実案件では Dapper をメインで使用しているわけですが、少しは使える場面もあるかもしれません。
例えばデバッガで使える DataSet ビジュアライザ。こういうやつですね。

f:id:s_earlgrey:20160221230034p:plain

これなら複数レコードのプロパティ/フィールドの値を一覧で見ることができます。単に CSV やタブ区切りテキストに吐き出すという手もありますが、デバッグ中にシームレスに見られるというのはやはり便利です。少々重いですが。

他はまぁ、WinForms や WebForms を使っている場合ですかね。特に前者の場合、DataGridView のソートやフィルタを使うには IBindingListIBindingListView インターフェースが必要なわけですが、これらを実装しているのはベースクラスライブラリでは 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 の要素を DataRow に変換してあげれば出来上がり。パフォーマンスに配慮して、毎回リフレクションを利用するのではなく、ExpressionTree でコンパイル済み getter を作成しています。
引数に 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 すらよくわかってないマンなので、今日のところはこれまで。